Skip to content

Commit

Permalink
SAML Login support
Browse files Browse the repository at this point in the history
  • Loading branch information
lubronzhan committed Aug 7, 2023
1 parent a2f8e74 commit 599114b
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 7 deletions.
2 changes: 1 addition & 1 deletion pkg/clustermodule/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func fetchSession(ctx *context.ClusterContext, params *session.Params) (*session
return nil, err
}

params = params.WithUserInfo(creds.Username, creds.Password)
params = params.WithUserInfo(creds.Username, creds.Password).WithCertificate([]byte(creds.UserCert)).WithKey([]byte(creds.UserKey))
return session.GetOrCreate(ctx, params)
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,15 @@ import (
const (
UsernameKey = "username"
PasswordKey = "password"
UserCertKey = "userCertificate"
UserKeyKey = "userKey"
)

type Credentials struct {
Username string
Password string
UserCert string
UserKey string
}

func GetCredentials(ctx context.Context, c client.Client, cluster *infrav1.VSphereCluster, controllerNamespace string) (*Credentials, error) {
Expand Down Expand Up @@ -103,6 +107,8 @@ func GetCredentials(ctx context.Context, c client.Client, cluster *infrav1.VSphe
credentials := &Credentials{
Username: getData(secret, UsernameKey),
Password: getData(secret, PasswordKey),
UserCert: getData(secret, UserCertKey),
UserKey: getData(secret, UserKeyKey),
}

return credentials, nil
Expand Down
6 changes: 6 additions & 0 deletions pkg/identity/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ var _ = Describe("GetCredentials", func() {
Expect(err).NotTo(HaveOccurred())
Expect(creds.Username).To(Equal(getData(credentialSecret, UsernameKey)))
Expect(creds.Password).To(Equal(getData(credentialSecret, PasswordKey)))
Expect(creds.UserCert).To(Equal(getData(credentialSecret, UserCertKey)))
Expect(creds.UserKey).To(Equal(getData(credentialSecret, UserKeyKey)))
})

It("should error if secret is not in the same namespace as the cluster", func() {
Expand Down Expand Up @@ -133,6 +135,8 @@ var _ = Describe("GetCredentials", func() {
Expect(err).NotTo(HaveOccurred())
Expect(creds.Username).To(Equal(getData(credentialSecret, UsernameKey)))
Expect(creds.Password).To(Equal(getData(credentialSecret, PasswordKey)))
Expect(creds.UserCert).To(Equal(getData(credentialSecret, UserCertKey)))
Expect(creds.UserKey).To(Equal(getData(credentialSecret, UserKeyKey)))
})

It("should error if allowedNamespaces is set to nil", func() {
Expand Down Expand Up @@ -262,6 +266,8 @@ func createSecret(namespace string) *corev1.Secret {
Data: map[string][]byte{
UsernameKey: []byte("user"),
PasswordKey: []byte("pass"),
UserCertKey: []byte("cert"),
UserKeyKey: []byte("key"),
},
}
Expect(k8sclient.Create(ctx, credentialSecret)).To(Succeed())
Expand Down
94 changes: 88 additions & 6 deletions pkg/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package session

import (
"context"
"crypto/tls"
"fmt"
"net/netip"
"net/url"
Expand All @@ -32,6 +33,7 @@ import (
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/session/keepalive"
"github.com/vmware/govmomi/sts"
"github.com/vmware/govmomi/vapi/rest"
"github.com/vmware/govmomi/vapi/tags"
"github.com/vmware/govmomi/vim25"
Expand Down Expand Up @@ -77,6 +79,8 @@ type Params struct {
userinfo *url.Userinfo
thumbprint string
feature Feature
userCert []byte
userKey []byte
}

func NewParams() *Params {
Expand Down Expand Up @@ -110,6 +114,16 @@ func (p *Params) WithFeatures(feature Feature) *Params {
return p
}

func (p *Params) WithCertificate(cert []byte) *Params {
p.userCert = cert
return p
}

func (p *Params) WithKey(key []byte) *Params {
p.userKey = key
return p
}

// GetOrCreate gets a cached session or creates a new one if one does not
// already exist.
func GetOrCreate(ctx context.Context, params *Params) (*Session, error) {
Expand Down Expand Up @@ -163,7 +177,8 @@ func GetOrCreate(ctx context.Context, params *Params) (*Session, error) {
}

soapURL.User = params.userinfo
client, err := newClient(ctx, logger, sessionKey, soapURL, params.thumbprint, params.feature)

client, err := newClient(ctx, logger, sessionKey, soapURL, params.thumbprint, params.feature, params.userCert, params.userKey)
if err != nil {
return nil, err
}
Expand All @@ -174,7 +189,7 @@ func GetOrCreate(ctx context.Context, params *Params) (*Session, error) {
// Assign the finder to the session.
session.Finder = find.NewFinder(session.Client.Client, false)
// Assign tag manager to the session.
manager, err := newManager(ctx, logger, sessionKey, client.Client, soapURL.User, params.feature)
manager, err := newManager(ctx, logger, sessionKey, client.Client, soapURL.User, params.feature, params.userCert, params.userKey)
if err != nil {
return nil, errors.Wrap(err, "unable to create tags manager")
}
Expand All @@ -197,7 +212,7 @@ func GetOrCreate(ctx context.Context, params *Params) (*Session, error) {
return &session, nil
}

func newClient(ctx context.Context, logger logr.Logger, sessionKey string, url *url.URL, thumbprint string, feature Feature) (*govmomi.Client, error) {
func newClient(ctx context.Context, logger logr.Logger, sessionKey string, url *url.URL, thumbprint string, feature Feature, userCert, userKey []byte) (*govmomi.Client, error) {
insecure := thumbprint == ""
soapClient := soap.NewClient(url, insecure)
if !insecure {
Expand Down Expand Up @@ -227,15 +242,15 @@ func newClient(ctx context.Context, logger logr.Logger, sessionKey string, url *
})
}

if err := c.Login(ctx, url.User); err != nil {
if err := login(ctx, logger, c, url.User, userCert, userKey); err != nil {
return nil, err
}

return c, nil
}

// newManager creates a Manager that encompasses the REST Client for the VSphere tagging API.
func newManager(ctx context.Context, logger logr.Logger, sessionKey string, client *vim25.Client, user *url.Userinfo, feature Feature) (*tags.Manager, error) {
func newManager(ctx context.Context, logger logr.Logger, sessionKey string, client *vim25.Client, user *url.Userinfo, feature Feature, userCert, userKey []byte) (*tags.Manager, error) {
rc := rest.NewClient(client)
if feature.EnableKeepAlive {
rc.Transport = keepalive.NewHandlerREST(rc, feature.KeepAliveDuration, func() error {
Expand All @@ -252,9 +267,11 @@ func newManager(ctx context.Context, logger logr.Logger, sessionKey string, clie
return errors.New("rest client session expired")
})
}
if err := rc.Login(ctx, user); err != nil {

if err := loginWithRestClient(ctx, logger, rc, client, user, userCert, userKey); err != nil {
return nil, err
}

return tags.NewManager(rc), nil
}

Expand Down Expand Up @@ -307,3 +324,68 @@ func (s *Session) findByUUID(ctx context.Context, uuid string, findByInstanceUUI
}
return ref, nil
}

func login(ctx context.Context, logger logr.Logger, client *govmomi.Client, user *url.Userinfo, userCert, userKey []byte) error {
if len(userCert) > 0 || len(userKey) > 0 {
// if certificate is configured, prefer using certificate
if user != nil {
logger.Info("Bother usrename/password and userCertificate/userKey are set. Using the userCertificate/userKey")
}

logger.V(4).Info("Session.LoginByToken with certificate", string(userCert))
signer, err := signer(ctx, logger, client.Client, userCert, userKey)
if err != nil {
return err
}

header := soap.Header{Security: signer}
return client.SessionManager.LoginByToken(client.WithHeader(ctx, header))
} else {

Check warning on line 343 in pkg/session/session.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)

Check warning on line 343 in pkg/session/session.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
return client.Login(ctx, user)
}
}

func loginWithRestClient(ctx context.Context, logger logr.Logger, rc *rest.Client, client *vim25.Client, user *url.Userinfo, userCert, userKey []byte) error {
if len(userCert) > 0 || len(userKey) > 0 {
// if certificate is configured, prefer using certificate
if user != nil {
logger.Info("Bother usrename/password and userCertificate/userKey are set. Using the userCertificate/userKey")
}
signer, err := signer(ctx, logger, client, userCert, userKey)
if err != nil {
return err
}
return rc.LoginByToken(rc.WithSigner(ctx, signer))
} else {

Check warning on line 359 in pkg/session/session.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)

Check warning on line 359 in pkg/session/session.go

View workflow job for this annotation

GitHub Actions / lint

indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)
return rc.Login(ctx, user)
}
}

// signer returns an sts.Signer for use with SAML token auth if connection is configured for such.
// Returns nil if username/password auth is configured for the connection.
func signer(ctx context.Context, logger logr.Logger, client *vim25.Client, cert, key []byte) (*sts.Signer, error) {
certificate, err := tls.X509KeyPair(cert, key)
if err != nil {
logger.Error(err, "Failed to load X509 key pair")
return nil, err
}

tokens, err := sts.NewClient(ctx, client)
if err != nil {
logger.Error(err, "Failed to create STS client")
return nil, err
}

req := sts.TokenRequest{
Certificate: &certificate,
Delegatable: true,
}

signer, err := tokens.Issue(ctx, req)
if err != nil {
logger.Error(err, "Failed to issue SAML token")
return nil, err
}

return signer, nil
}

0 comments on commit 599114b

Please sign in to comment.