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

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