Pass user authentication to Tiller (#4)

pull/2012/head
saumanbiswas 9 years ago committed by sauman
parent fc315ab598
commit 6f06945db4

@ -86,7 +86,6 @@ func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...
req.DisableHooks = h.opts.disableHooks req.DisableHooks = h.opts.disableHooks
req.ReuseName = h.opts.reuseName req.ReuseName = h.opts.reuseName
ctx := NewContext() ctx := NewContext()
if h.opts.before != nil { if h.opts.before != nil {
if err := h.opts.before(ctx, req); err != nil { if err := h.opts.before(ctx, req); err != nil {
return nil, err return nil, err

@ -17,14 +17,20 @@ limitations under the License.
package helm package helm
import ( import (
"bytes"
"encoding/base64"
"io/ioutil"
"log"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/spf13/pflag"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
cpb "k8s.io/helm/pkg/proto/hapi/chart" cpb "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
rls "k8s.io/helm/pkg/proto/hapi/services" rls "k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/version" "k8s.io/helm/pkg/version"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
) )
// Option allows specifying various settings configurable by // Option allows specifying various settings configurable by
@ -383,10 +389,94 @@ func WithMaxHistory(max int32) HistoryOption {
} }
} }
// NewContext creates a versioned context. // NewContext creates a versioned context with kubernetes client data.
func NewContext() context.Context { func NewContext() context.Context {
md := metadata.Pairs("x-helm-api-client", version.Version) return metadata.NewContext(
return metadata.NewContext(context.TODO(), md) 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
} }
// ReleaseTestOption allows configuring optional request data for // ReleaseTestOption allows configuring optional request data for

@ -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"
)

@ -17,6 +17,10 @@ limitations under the License.
package tiller package tiller
import ( import (
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@ -24,8 +28,13 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/version" "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. // 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 { func newUnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 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 // whitelist GetVersion() from the version check
if _, m := splitMethod(info.FullMethod); m != "GetVersion" { if _, m := splitMethod(info.FullMethod); m != "GetVersion" {
log.Println(err) log.Println(err)
return nil, err return nil, err
} }
} }
ctx, err = authenticate(ctx)
if err != nil {
log.Println(err)
return nil, err
}
return handler(ctx, req) return handler(ctx, req)
} }
} }
func newStreamInterceptor() grpc.StreamServerInterceptor { func newStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 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) log.Println(err)
return err return err
} }
// TODO: How to pass modified ctx?
return handler(srv, ss) return handler(srv, ss)
} }
} }
@ -87,3 +146,190 @@ func checkClientVersion(ctx context.Context) error {
} }
return nil 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
}

Loading…
Cancel
Save