vSphere Cloud Provider: add SAML token authentication support

For now the config structs and validation are left as-is and
the LoginByToken method is used if the username value is PEM encoded.
In this case of username field configured with the public key, the password
field is expected to be configured with the private key.

In a follow-up PR we can look at collapsing the auth related fields into
a common struct to avoid duplication of field merging and validation.
And then add separate fields for the public and private keys.

Fixes #63209
This commit is contained in:
Doug MacEachern 2018-05-14 13:01:42 -07:00
parent 41a531317a
commit 83768d286c
2 changed files with 130 additions and 13 deletions

View File

@ -18,12 +18,15 @@ package vclib
import (
"context"
"crypto/tls"
"encoding/pem"
"net"
neturl "net/url"
"sync"
"github.com/golang/glog"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/sts"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/soap"
)
@ -78,6 +81,49 @@ func (connection *VSphereConnection) Connect(ctx context.Context) error {
return nil
}
// login calls SessionManager.LoginByToken if certificate and private key are configured,
// otherwise calls SessionManager.Login with user and password.
func (connection *VSphereConnection) login(ctx context.Context, client *vim25.Client) error {
m := session.NewManager(client)
// TODO: Add separate fields for certificate and private-key.
// For now we can leave the config structs and validation as-is and
// decide to use LoginByToken if the username value is PEM encoded.
b, _ := pem.Decode([]byte(connection.Username))
if b == nil {
glog.V(3).Infof("SessionManager.Login with username '%s'", connection.Username)
return m.Login(ctx, neturl.UserPassword(connection.Username, connection.Password))
}
glog.V(3).Infof("SessionManager.LoginByToken with certificate '%s'", connection.Username)
cert, err := tls.X509KeyPair([]byte(connection.Username), []byte(connection.Password))
if err != nil {
glog.Errorf("Failed to load X509 key pair. err: %+v", err)
return err
}
tokens, err := sts.NewClient(ctx, client)
if err != nil {
glog.Errorf("Failed to create STS client. err: %+v", err)
return err
}
req := sts.TokenRequest{
Certificate: &cert,
}
signer, err := tokens.Issue(ctx, req)
if err != nil {
glog.Errorf("Failed to issue SAML token. err: %+v", err)
return err
}
header := soap.Header{Security: signer}
return m.LoginByToken(client.WithHeader(ctx, header))
}
// Logout calls SessionManager.Logout for the given connection.
func (connection *VSphereConnection) Logout(ctx context.Context) {
m := session.NewManager(connection.Client)
@ -100,13 +146,16 @@ func (connection *VSphereConnection) NewClient(ctx context.Context) (*vim25.Clie
glog.Errorf("Failed to create new client. err: %+v", err)
return nil, err
}
m := session.NewManager(client)
err = m.Login(ctx, neturl.UserPassword(connection.Username, connection.Password))
err = connection.login(ctx, client)
if err != nil {
return nil, err
}
if glog.V(3) {
s, err := session.NewManager(client).UserSession(ctx)
if err == nil {
glog.Infof("New session ID for '%s' = %s", s.UserName, s.Key)
}
}
if connection.RoundTripperCount == 0 {
connection.RoundTripperCount = RoundTripperDefaultCount

View File

@ -25,13 +25,41 @@ import (
"strings"
"testing"
lookup "github.com/vmware/govmomi/lookup/simulator"
"github.com/vmware/govmomi/simulator"
"github.com/vmware/govmomi/simulator/vpx"
sts "github.com/vmware/govmomi/sts/simulator"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/cloudprovider/providers/vsphere/vclib"
)
// localhostCert was generated from crypto/tls/generate_cert.go with the following command:
// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var localhostCert = `-----BEGIN CERTIFICATE-----
MIIBjzCCATmgAwIBAgIRAKpi2WmTcFrVjxrl5n5YDUEwDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
QQC9fEbRszP3t14Gr4oahV7zFObBI4TfA5i7YnlMXeLinb7MnvT4bkfOJzE6zktn
59zP7UiHs3l4YOuqrjiwM413AgMBAAGjaDBmMA4GA1UdDwEB/wQEAwICpDATBgNV
HSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MC4GA1UdEQQnMCWCC2V4
YW1wbGUuY29thwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUA
A0EAUsVE6KMnza/ZbodLlyeMzdo7EM/5nb5ywyOxgIOCf0OOLHsPS9ueGLQX9HEG
//yjTXuhNcUugExIjM/AIwAZPQ==
-----END CERTIFICATE-----`
// localhostKey is the private key for localhostCert.
var localhostKey = `-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAL18RtGzM/e3XgavihqFXvMU5sEjhN8DmLtieUxd4uKdvsye9Phu
R84nMTrOS2fn3M/tSIezeXhg66quOLAzjXcCAwEAAQJBAKcRxH9wuglYLBdI/0OT
BLzfWPZCEw1vZmMR2FF1Fm8nkNOVDPleeVGTWoOEcYYlQbpTmkGSxJ6ya+hqRi6x
goECIQDx3+X49fwpL6B5qpJIJMyZBSCuMhH4B7JevhGGFENi3wIhAMiNJN5Q3UkL
IuSvv03kaPR5XVQ99/UeEetUgGvBcABpAiBJSBzVITIVCGkGc7d+RCf49KTCIklv
bGWObufAR8Ni4QIgWpILjW8dkGg8GOUZ0zaNA6Nvt6TIv2UWGJ4v5PoV98kCIQDx
rIiZs5QbKdycsv9gQJzwQAogC8o04X3Zz3dsoX+h4A==
-----END RSA PRIVATE KEY-----`
func configFromEnv() (cfg VSphereConfig, ok bool) {
var InsecureFlag bool
var err error
@ -61,14 +89,9 @@ func configFromEnv() (cfg VSphereConfig, ok bool) {
return
}
// configFromEnvOrSim returns config from configFromEnv if set,
// otherwise starts a vcsim instance and returns config for use against the vcsim instance.
func configFromEnvOrSim() (VSphereConfig, func()) {
cfg, ok := configFromEnv()
if ok {
return cfg, func() {}
}
// configFromSim starts a vcsim instance and returns config for use against the vcsim instance.
func configFromSim() (VSphereConfig, func()) {
var cfg VSphereConfig
model := simulator.VPX()
err := model.Create()
@ -79,6 +102,13 @@ func configFromEnvOrSim() (VSphereConfig, func()) {
model.Service.TLS = new(tls.Config)
s := model.Service.NewServer()
// STS simulator
path, handler := sts.New(s.URL, vpx.Setting)
model.Service.ServeMux.Handle(path, handler)
// Lookup Service simulator
model.Service.RegisterSDK(lookup.New())
cfg.Global.InsecureFlag = true
cfg.Global.VCenterIP = s.URL.Hostname()
cfg.Global.VCenterPort = s.URL.Port()
@ -105,6 +135,15 @@ func configFromEnvOrSim() (VSphereConfig, func()) {
}
}
// configFromEnvOrSim returns config from configFromEnv if set, otherwise returns configFromSim.
func configFromEnvOrSim() (VSphereConfig, func()) {
cfg, ok := configFromEnv()
if ok {
return cfg, func() {}
}
return configFromSim()
}
func TestReadConfig(t *testing.T) {
_, err := readConfig(nil)
if err == nil {
@ -179,7 +218,36 @@ func TestVSphereLogin(t *testing.T) {
if err != nil {
t.Errorf("Failed to connect to vSphere: %s", err)
}
defer vcInstance.conn.Logout(ctx)
vcInstance.conn.Logout(ctx)
}
func TestVSphereLoginByToken(t *testing.T) {
cfg, cleanup := configFromSim()
defer cleanup()
// Configure for SAML token auth
cfg.Global.User = localhostCert
cfg.Global.Password = localhostKey
// Create vSphere configuration object
vs, err := newControllerNode(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate vSphere: %s", err)
}
ctx := context.Background()
// Create vSphere client
vcInstance, ok := vs.vsphereInstanceMap[cfg.Global.VCenterIP]
if !ok {
t.Fatalf("Couldn't get vSphere instance: %s", cfg.Global.VCenterIP)
}
err = vcInstance.conn.Connect(ctx)
if err != nil {
t.Errorf("Failed to connect to vSphere: %s", err)
}
vcInstance.conn.Logout(ctx)
}
func TestZones(t *testing.T) {