github.com/vmware/govmomi@v0.51.0/sts/client.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package sts
     6  
     7  import (
     8  	"context"
     9  	"crypto/tls"
    10  	"errors"
    11  	"fmt"
    12  	"net/url"
    13  	"time"
    14  
    15  	internalhelpers "github.com/vmware/govmomi/internal"
    16  	"github.com/vmware/govmomi/lookup"
    17  	"github.com/vmware/govmomi/lookup/types"
    18  	"github.com/vmware/govmomi/sts/internal"
    19  	"github.com/vmware/govmomi/vim25"
    20  	"github.com/vmware/govmomi/vim25/soap"
    21  	vim25types "github.com/vmware/govmomi/vim25/types"
    22  )
    23  
    24  const (
    25  	Namespace  = "oasis:names:tc:SAML:2.0:assertion"
    26  	basePath   = "/sts"
    27  	Path       = basePath + "/STSService"
    28  	SystemPath = basePath + "/system-STSService/sdk"
    29  )
    30  
    31  // Client is a soap.Client targeting the STS (Secure Token Service) API endpoint.
    32  type Client struct {
    33  	*soap.Client
    34  
    35  	RoundTripper soap.RoundTripper
    36  }
    37  
    38  func getEndpointURL(ctx context.Context, c *vim25.Client) string {
    39  	// Services running on vCenter can bypass lookup service using the
    40  	// system-STSService path. This avoids the need to lookup the system domain.
    41  	if usingSidecar := internalhelpers.UsingEnvoySidecar(c); usingSidecar {
    42  		return fmt.Sprintf("http://%s%s", c.URL().Host, SystemPath)
    43  	}
    44  	return getEndpointURLFromLookupService(ctx, c)
    45  }
    46  
    47  func getEndpointURLFromLookupService(ctx context.Context, c *vim25.Client) string {
    48  	filter := &types.LookupServiceRegistrationFilter{
    49  		ServiceType: &types.LookupServiceRegistrationServiceType{
    50  			Product: "com.vmware.cis",
    51  			Type:    "cs.identity",
    52  		},
    53  		EndpointType: &types.LookupServiceRegistrationEndpointType{
    54  			Protocol: "wsTrust",
    55  			Type:     "com.vmware.cis.cs.identity.sso",
    56  		},
    57  	}
    58  
    59  	return lookup.EndpointURL(ctx, c, Path, filter)
    60  }
    61  
    62  // NewClient returns a client targeting the STS API endpoint.
    63  // The Client.URL will be set to that of the Lookup Service's endpoint registration,
    64  // as the SSO endpoint can be external to vCenter.  If the Lookup Service is not available,
    65  // URL defaults to Path on the vim25.Client.URL.Host.
    66  func NewClient(ctx context.Context, c *vim25.Client) (*Client, error) {
    67  
    68  	url := getEndpointURL(ctx, c)
    69  	sc := c.Client.NewServiceClient(url, Namespace)
    70  
    71  	return &Client{sc, sc}, nil
    72  }
    73  
    74  // RoundTrip dispatches to the RoundTripper field.
    75  func (c *Client) RoundTrip(ctx context.Context, req, res soap.HasFault) error {
    76  	// Drop any operationID header, not used by STS
    77  	ctx = context.WithValue(ctx, vim25types.ID{}, "")
    78  	return c.RoundTripper.RoundTrip(ctx, req, res)
    79  }
    80  
    81  // TokenRequest parameters for issuing a SAML token.
    82  // At least one of Userinfo or Certificate must be specified.
    83  // When `TokenRequest.Certificate` is set, the `tls.Certificate.PrivateKey` field must be set as it is required to sign the request.
    84  // When the `tls.Certificate.Certificate` field is not set, the request Assertion header is set to that of the TokenRequest.Token.
    85  // Otherwise `tls.Certificate.Certificate` is used as the BinarySecurityToken in the request.
    86  type TokenRequest struct {
    87  	Userinfo    *url.Userinfo    // Userinfo when set issues a Bearer token
    88  	Certificate *tls.Certificate // Certificate when set issues a HoK token
    89  	Lifetime    time.Duration    // Lifetime is the token's lifetime, defaults to 10m
    90  	Renewable   bool             // Renewable allows the issued token to be renewed
    91  	Delegatable bool             // Delegatable allows the issued token to be delegated (e.g. for use with ActAs)
    92  	ActAs       bool             // ActAs allows to request an ActAs token based on the passed Token.
    93  	Token       string           // Token for Renew request or Issue request ActAs identity or to be exchanged.
    94  	KeyType     string           // KeyType for requested token (if not set will be decucted from Userinfo and Certificate options)
    95  	KeyID       string           // KeyID used for signing the requests
    96  }
    97  
    98  func (c *Client) newRequest(req TokenRequest, kind string, s *Signer) (internal.RequestSecurityToken, error) {
    99  	if req.Lifetime == 0 {
   100  		req.Lifetime = 5 * time.Minute
   101  	}
   102  
   103  	created := time.Now().UTC()
   104  	rst := internal.RequestSecurityToken{
   105  		TokenType:          c.Namespace,
   106  		RequestType:        "http://docs.oasis-open.org/ws-sx/ws-trust/200512/" + kind,
   107  		SignatureAlgorithm: internal.SHA256,
   108  		Lifetime: &internal.Lifetime{
   109  			Created: created.Format(internal.Time),
   110  			Expires: created.Add(req.Lifetime).Format(internal.Time),
   111  		},
   112  		Renewing: &internal.Renewing{
   113  			Allow: req.Renewable,
   114  			// /wst:RequestSecurityToken/wst:Renewing/@OK
   115  			// "It NOT RECOMMENDED to use this as it can leave you open to certain types of security attacks.
   116  			// Issuers MAY restrict the period after expiration during which time the token can be renewed.
   117  			// This window is governed by the issuer's policy."
   118  			OK: false,
   119  		},
   120  		Delegatable: req.Delegatable,
   121  		KeyType:     req.KeyType,
   122  	}
   123  
   124  	if req.KeyType == "" {
   125  		// Deduce KeyType based on Certificate nad Userinfo.
   126  		if req.Certificate == nil {
   127  			if req.Userinfo == nil {
   128  				return rst, errors.New("one of TokenRequest Certificate or Userinfo is required")
   129  			}
   130  			rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer"
   131  		} else {
   132  			rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey"
   133  			// For HOK KeyID is required.
   134  			if req.KeyID == "" {
   135  				req.KeyID = newID()
   136  			}
   137  		}
   138  	}
   139  
   140  	if req.KeyID != "" {
   141  		rst.UseKey = &internal.UseKey{Sig: req.KeyID}
   142  		s.keyID = rst.UseKey.Sig
   143  	}
   144  
   145  	return rst, nil
   146  }
   147  
   148  func (s *Signer) setLifetime(lifetime *internal.Lifetime) error {
   149  	var err error
   150  	if lifetime != nil {
   151  		s.Lifetime.Created, err = time.Parse(internal.Time, lifetime.Created)
   152  		if err == nil {
   153  			s.Lifetime.Expires, err = time.Parse(internal.Time, lifetime.Expires)
   154  		}
   155  	}
   156  	return err
   157  }
   158  
   159  // Issue is used to request a security token.
   160  // The returned Signer can be used to sign SOAP requests, such as the SessionManager LoginByToken method and the RequestSecurityToken method itself.
   161  // One of TokenRequest Certificate or Userinfo is required, with Certificate taking precedence.
   162  // When Certificate is set, a Holder-of-Key token will be requested.  Otherwise, a Bearer token is requested with the Userinfo credentials.
   163  // See: http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/errata01/os/ws-trust-1.4-errata01-os-complete.html#_Toc325658937
   164  func (c *Client) Issue(ctx context.Context, req TokenRequest) (*Signer, error) {
   165  	s := &Signer{
   166  		Certificate: req.Certificate,
   167  		keyID:       req.KeyID,
   168  		Token:       req.Token,
   169  		user:        req.Userinfo,
   170  	}
   171  
   172  	rst, err := c.newRequest(req, "Issue", s)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	if req.ActAs {
   178  		rst.ActAs = &internal.Target{
   179  			Token: req.Token,
   180  		}
   181  	}
   182  
   183  	header := soap.Header{
   184  		Security: s,
   185  		Action:   fmt.Sprintf(`"%s"`, rst.Action()),
   186  	}
   187  
   188  	res, err := internal.Issue(c.WithHeader(ctx, header), c, &rst)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	s.Token = res.RequestSecurityTokenResponse.RequestedSecurityToken.Assertion
   194  
   195  	return s, s.setLifetime(res.RequestSecurityTokenResponse.Lifetime)
   196  }
   197  
   198  // Renew is used to request a security token renewal.
   199  func (c *Client) Renew(ctx context.Context, req TokenRequest) (*Signer, error) {
   200  	s := &Signer{
   201  		Certificate: req.Certificate,
   202  	}
   203  
   204  	rst, err := c.newRequest(req, "Renew", s)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	if req.Token == "" {
   210  		return nil, errors.New("TokenRequest Token is required")
   211  	}
   212  
   213  	rst.RenewTarget = &internal.Target{Token: req.Token}
   214  
   215  	header := soap.Header{
   216  		Security: s,
   217  		Action:   rst.Action(),
   218  	}
   219  
   220  	res, err := internal.Renew(c.WithHeader(ctx, header), c, &rst)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	s.Token = res.RequestedSecurityToken.Assertion
   226  
   227  	return s, s.setLifetime(res.Lifetime)
   228  }