From 18061ac8e343532b8b7a4a3d7759f81c0137060a Mon Sep 17 00:00:00 2001 From: saumanbiswas Date: Thu, 9 Feb 2017 22:10:12 +0600 Subject: [PATCH] Pass user authentication to Tiller (#4) --- pkg/helm/client.go | 1 - pkg/helm/option.go | 98 ++++++++++++++++- pkg/helm/types.go | 13 +++ pkg/tiller/server.go | 252 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 pkg/helm/types.go diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 049c6af60..7bf2b2d7f 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -84,7 +84,6 @@ func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ... req.DisableHooks = h.opts.disableHooks req.ReuseName = h.opts.reuseName ctx := NewContext() - if h.opts.before != nil { if err := h.opts.before(ctx, req); err != nil { return nil, err diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 42df562cf..45a707f44 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -17,14 +17,20 @@ limitations under the License. package helm import ( + "bytes" + "encoding/base64" + "io/ioutil" + "log" + "github.com/golang/protobuf/proto" + "github.com/spf13/pflag" "golang.org/x/net/context" "google.golang.org/grpc/metadata" - cpb "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/version" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) // Option allows specifying various settings configurable by @@ -359,8 +365,92 @@ func WithMaxHistory(max int32) HistoryOption { } } -// NewContext creates a versioned context. +// NewContext creates a versioned context with kubernetes client data. func NewContext() context.Context { - md := metadata.Pairs("x-helm-api-client", version.Version) - return metadata.NewContext(context.TODO(), md) + return metadata.NewContext( + context.TODO(), + metadata.Join( + metadata.New( + extractKubeConfig()), + metadata.New(map[string]string{ + "x-helm-api-client": version.Version, + }), + ), + ) +} + +func extractKubeConfig() map[string]string { + configData := make(map[string]string) + clientConfig := cmdutil.DefaultClientConfig(pflag.NewFlagSet("", pflag.ContinueOnError)) + c, err := clientConfig.ClientConfig() + if err != nil { + log.Println("Failed to extract kubeconfig") + return configData + } + + // Kube APIServer URL + if len(c.Host) != 0 { + configData[K8sServer] = c.Host + } + + if c.AuthProvider != nil { + switch c.AuthProvider.Name { + case "gcp": + configData[Authorization] = "Bearer " + c.AuthProvider.Config["access_token"] + case "oidc": + configData[Authorization] = "Bearer " + c.AuthProvider.Config["id-token"] + default: + panic("Unknown auth provider: " + c.AuthProvider.Name) + } + } + + if len(c.BearerToken) != 0 { + configData[Authorization] = "Bearer " + c.BearerToken + } + + if len(c.Username) != 0 && len(c.Password) != 0 { + configData[Authorization] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.Username+":"+c.Password)) + } + + if len(string(c.CAData)) != 0 { + configData[K8sCertificateAuthority] = base64.StdEncoding.EncodeToString(bytes.TrimSpace(c.CAData)) + } + + if len(string(c.TLSClientConfig.KeyData)) != 0 { + configData[K8sClientKey] = base64.StdEncoding.EncodeToString(c.TLSClientConfig.KeyData) + } + + if len(string(c.TLSClientConfig.CertData)) != 0 { + configData[K8sClientCertificate] = base64.StdEncoding.EncodeToString(c.TLSClientConfig.CertData) + } + + if len(c.TLSClientConfig.CAFile) != 0 { + b, err := ioutil.ReadFile(c.TLSClientConfig.CAFile) + if err != nil { + log.Println(err) + } else { + configData[K8sCertificateAuthority] = base64.StdEncoding.EncodeToString(b) + } + } + + if len(c.TLSClientConfig.CertFile) != 0 { + b, err := ioutil.ReadFile(c.TLSClientConfig.CertFile) + if err != nil { + log.Println(err) + } else { + configData[K8sClientCertificate] = base64.StdEncoding.EncodeToString(b) + } + } + + if len(c.TLSClientConfig.KeyFile) != 0 { + if len(c.TLSClientConfig.KeyFile) != 0 { + b, err := ioutil.ReadFile(c.TLSClientConfig.KeyFile) + if err != nil { + log.Println(err) + } else { + configData[K8sClientKey] = base64.StdEncoding.EncodeToString(b) + } + } + } + return configData } diff --git a/pkg/helm/types.go b/pkg/helm/types.go new file mode 100644 index 000000000..b13d18166 --- /dev/null +++ b/pkg/helm/types.go @@ -0,0 +1,13 @@ +package helm + +const ( + Authorization = "authorization" + K8sServer = "k8s-server" + K8sClientCertificate = "k8s-client-certificate" + K8sCertificateAuthority = "k8s-certificate-authority" + K8sClientKey = "k8s-client-key" + + // Generated from input keys above + K8sUser = "k8s-user" + K8sConfig = "k8s-client-config" +) diff --git a/pkg/tiller/server.go b/pkg/tiller/server.go index 6cecda70f..c28d67a8c 100644 --- a/pkg/tiller/server.go +++ b/pkg/tiller/server.go @@ -17,6 +17,10 @@ limitations under the License. package tiller import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" "fmt" "log" "strings" @@ -24,8 +28,13 @@ import ( "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/metadata" - + "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/version" + authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + rest "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" ) // maxMsgSize use 10MB as the default message size limit. @@ -41,25 +50,75 @@ func NewServer() *grpc.Server { ) } +func authenticate(ctx context.Context) (context.Context, error) { + md, ok := metadata.FromContext(ctx) + if !ok { + return nil, errors.New("Missing metadata in context.") + } + + var user *authenticationapi.UserInfo + var kubeConfig *rest.Config + var err error + authHeader, ok := md[helm.Authorization] + if !ok || authHeader[0] == "" { + user, kubeConfig, err = checkClientCert(ctx) + } else { + if strings.HasPrefix(authHeader[0], "Bearer ") { + user, kubeConfig, err = checkBearerAuth(ctx) + } else if strings.HasPrefix(authHeader[0], "Basic ") { + user, kubeConfig, err = checkBasicAuth(ctx) + } else { + return nil, errors.New("Unknown authorization scheme.") + } + } + if err != nil { + return nil, err + } + ctx = context.WithValue(ctx, helm.K8sUser, user) + ctx = context.WithValue(ctx, helm.K8sConfig, kubeConfig) + + // TODO: Remove + if user == nil { + log.Println("user not found in context") + } else { + log.Println("authenticated user:", user) + } + return ctx, nil +} + func newUnaryInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - if err := checkClientVersion(ctx); err != nil { + err = checkClientVersion(ctx) + if err != nil { // whitelist GetVersion() from the version check if _, m := splitMethod(info.FullMethod); m != "GetVersion" { log.Println(err) return nil, err } } + ctx, err = authenticate(ctx) + if err != nil { + log.Println(err) + return nil, err + } return handler(ctx, req) } } func newStreamInterceptor() grpc.StreamServerInterceptor { return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if err := checkClientVersion(ss.Context()); err != nil { + ctx := ss.Context() + err := checkClientVersion(ctx) + if err != nil { + log.Println(err) + return err + } + ctx, err = authenticate(ctx) + if err != nil { log.Println(err) return err } + // TODO: How to pass modified ctx? return handler(srv, ss) } } @@ -87,3 +146,190 @@ func checkClientVersion(ctx context.Context) error { } return nil } + +func checkBearerAuth(ctx context.Context) (*authenticationapi.UserInfo, *rest.Config, error) { + md, _ := metadata.FromContext(ctx) + token := md[helm.Authorization][0][len("Bearer "):] + + apiServer, err := getServerURL(md) + if err != nil { + return nil, nil, err + } + caCert, _ := getCertificateAuthority(md) + + // TODO: Should be InClusterConfig() ? + kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{DefaultClientConfig: &clientcmd.DefaultClientConfig}, + &clientcmd.ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{ + Server: apiServer, + CertificateAuthorityData: caCert, + }}).ClientConfig() + if err != nil { + return nil, nil, err + } + + client, err := clientset.NewForConfig(kubeConfig) + if err != nil { + return nil, nil, err + } + + // verify token + tokenReq := &authenticationapi.TokenReview{ + Spec: authenticationapi.TokenReviewSpec{ + Token: token, + }, + } + result, err := client.AuthenticationClient.TokenReviews().Create(tokenReq) + if err != nil { + return nil, nil, err + } + if !result.Status.Authenticated { + return nil, nil, errors.New("Not authenticated") + } + kubeConfig.BearerToken = token + return &result.Status.User, kubeConfig, nil +} + +func checkBasicAuth(ctx context.Context) (*authenticationapi.UserInfo, *rest.Config, error) { + md, _ := metadata.FromContext(ctx) + authz := md[helm.Authorization][0] + + apiServer, err := getServerURL(md) + if err != nil { + return nil, nil, err + } + basicAuth, err := base64.StdEncoding.DecodeString(authz[len("Basic "):]) + if err != nil { + return nil, nil, err + } + username, password := getUserPasswordFromBasicAuth(string(basicAuth)) + if len(username) == 0 || len(password) == 0 { + return nil, nil, errors.New("Missing username or password.") + } + kubeConfig := &rest.Config{ + Host: apiServer, + Username: username, + Password: password, + } + caCert, err := getCertificateAuthority(md) + if err == nil { + kubeConfig.TLSClientConfig = rest.TLSClientConfig{ + CAData: caCert, + } + } + + client, err := clientset.NewForConfig(kubeConfig) + if err != nil { + return nil, nil, err + } + + // verify credentials + _, err = client.DiscoveryClient.ServerVersion() + if err != nil { + return nil, nil, err + } + + return &authenticationapi.UserInfo{ + Username: username, + }, kubeConfig, nil +} + +func getUserPasswordFromBasicAuth(token string) (string, string) { + st := strings.SplitN(token, ":", 2) + if len(st) == 2 { + return st[0], st[1] + } + return "", "" +} + +func checkClientCert(ctx context.Context) (*authenticationapi.UserInfo, *rest.Config, error) { + md, _ := metadata.FromContext(ctx) + + apiServer, err := getServerURL(md) + if err != nil { + return nil, nil, err + } + kubeConfig := &rest.Config{ + Host: apiServer, + } + crt, err := getClientCert(md) + if err != nil { + return nil, nil, err + } + key, err := getClientKey(md) + if err != nil { + return nil, nil, err + } + kubeConfig.TLSClientConfig = rest.TLSClientConfig{ + KeyData: key, + CertData: crt, + } + caCert, err := getCertificateAuthority(md) + if err == nil { + kubeConfig.TLSClientConfig.CAData = caCert + } + client, err := clientset.NewForConfig(kubeConfig) + if err != nil { + return nil, nil, err + } + + // verify credentials + _, err = client.DiscoveryClient.ServerVersion() + if err != nil { + return nil, nil, err + } + + pem, _ := pem.Decode([]byte(crt)) + c, err := x509.ParseCertificate(pem.Bytes) + if err != nil { + return nil, nil, err + } + + return &authenticationapi.UserInfo{ + Username: c.Subject.CommonName, + }, kubeConfig, nil +} + +func getClientCert(md metadata.MD) ([]byte, error) { + cert, ok := md[helm.K8sClientCertificate] + if !ok { + return nil, errors.New("Client certificate not found") + } + certData, err := base64.StdEncoding.DecodeString(cert[0]) + if err != nil { + return nil, err + } + return certData, nil +} + +func getClientKey(md metadata.MD) ([]byte, error) { + key, ok := md[helm.K8sClientKey] + if !ok { + return nil, errors.New("Client key not found") + } + keyData, err := base64.StdEncoding.DecodeString(key[0]) + if err != nil { + return nil, err + } + return keyData, nil +} + +func getCertificateAuthority(md metadata.MD) ([]byte, error) { + caData, ok := md[helm.K8sCertificateAuthority] + if !ok { + return nil, errors.New("CAcert not found") + } + caCert, err := base64.StdEncoding.DecodeString(caData[0]) + if err != nil { + return nil, err + } + return caCert, nil +} + +func getServerURL(md metadata.MD) (string, error) { + apiserver, ok := md[helm.K8sServer] + if !ok { + return "", errors.New("API server url not found") + } + return apiserver[0], nil +}