github.com/vmware/go-vcloud-director/v2@v2.24.0/govcd/api_vcd.go (about)

     1  /*
     2   * Copyright 2019 VMware, Inc.  All rights reserved.  Licensed under the Apache v2 License.
     3   */
     4  
     5  package govcd
     6  
     7  import (
     8  	"crypto/tls"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"strings"
    15  	"time"
    16  
    17  	semver "github.com/hashicorp/go-version"
    18  
    19  	"github.com/vmware/go-vcloud-director/v2/types/v56"
    20  	"github.com/vmware/go-vcloud-director/v2/util"
    21  )
    22  
    23  // VCDClientOption defines signature for customizing VCDClient using
    24  // functional options pattern.
    25  type VCDClientOption func(*VCDClient) error
    26  
    27  type VCDClient struct {
    28  	Client      Client  // Client for the underlying VCD instance
    29  	sessionHREF url.URL // HREF for the session API
    30  	QueryHREF   url.URL // HREF for the query API
    31  }
    32  
    33  func (vcdClient *VCDClient) vcdloginurl() error {
    34  	if err := vcdClient.Client.validateAPIVersion(); err != nil {
    35  		return fmt.Errorf("could not find valid version for login: %s", err)
    36  	}
    37  
    38  	// find login address matching the API version
    39  	var neededVersion VersionInfo
    40  	for _, versionInfo := range vcdClient.Client.supportedVersions.VersionInfos {
    41  		if versionInfo.Version == vcdClient.Client.APIVersion {
    42  			neededVersion = versionInfo
    43  			break
    44  		}
    45  	}
    46  
    47  	loginUrl, err := url.Parse(neededVersion.LoginUrl)
    48  	if err != nil {
    49  		return fmt.Errorf("couldn't find a LoginUrl for version %s", vcdClient.Client.APIVersion)
    50  	}
    51  	vcdClient.sessionHREF = *loginUrl
    52  	return nil
    53  }
    54  
    55  // vcdCloudApiAuthorize performs the authorization to VCD using open API
    56  func (vcdClient *VCDClient) vcdCloudApiAuthorize(user, pass, org string) (*http.Response, error) {
    57  	var missingItems []string
    58  	if user == "" {
    59  		missingItems = append(missingItems, "user")
    60  	}
    61  	if pass == "" {
    62  		missingItems = append(missingItems, "password")
    63  	}
    64  	if org == "" {
    65  		missingItems = append(missingItems, "org")
    66  	}
    67  	if len(missingItems) > 0 {
    68  		return nil, fmt.Errorf("authorization is not possible because of these missing items: %v", missingItems)
    69  	}
    70  
    71  	util.Logger.Println("[TRACE] Connecting to VCD using cloudapi")
    72  	// This call can only be used by tenants
    73  	rawUrl := vcdClient.sessionHREF.Scheme + "://" + vcdClient.sessionHREF.Host + "/cloudapi/1.0.0/sessions"
    74  
    75  	// If we are connecting as provider, we need to qualify the request.
    76  	if strings.EqualFold(org, "system") {
    77  		rawUrl += "/provider"
    78  	}
    79  	util.Logger.Printf("[TRACE] URL %s\n", rawUrl)
    80  	loginUrl, err := url.Parse(rawUrl)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("error parsing URL %s", rawUrl)
    83  	}
    84  	vcdClient.sessionHREF = *loginUrl
    85  	req := vcdClient.Client.NewRequest(map[string]string{}, http.MethodPost, *loginUrl, nil)
    86  	// Set Basic Authentication Header
    87  	req.SetBasicAuth(user+"@"+org, pass)
    88  	// Add the Accept header. The version must be at least 33.0 for cloudapi to work
    89  	req.Header.Add("Accept", "application/*;version="+vcdClient.Client.APIVersion)
    90  	resp, err := vcdClient.Client.Http.Do(req)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	defer func(Body io.ReadCloser) {
    96  		err := Body.Close()
    97  		if err != nil {
    98  			util.Logger.Printf("error closing response Body [vcdCloudApiAuthorize]: %s", err)
    99  		}
   100  	}(resp.Body)
   101  
   102  	// Catch HTTP 401 (Status Unauthorized) to return an error as otherwise this library would return
   103  	// odd errors while doing lookup of resources and confuse user.
   104  	if resp.StatusCode == http.StatusUnauthorized {
   105  		return nil, fmt.Errorf("received response HTTP %d (Unauthorized). Please check if your credentials are valid",
   106  			resp.StatusCode)
   107  	}
   108  
   109  	// Store the authorization header
   110  	vcdClient.Client.VCDToken = resp.Header.Get(BearerTokenHeader)
   111  	vcdClient.Client.VCDAuthHeader = BearerTokenHeader
   112  	vcdClient.Client.IsSysAdmin = strings.EqualFold(org, "system")
   113  	// Get query href
   114  	vcdClient.QueryHREF = vcdClient.Client.VCDHREF
   115  	vcdClient.QueryHREF.Path += "/query"
   116  	return resp, nil
   117  }
   118  
   119  // NewVCDClient initializes VMware VMware Cloud Director client with reasonable defaults.
   120  // It accepts functions of type VCDClientOption for adjusting defaults.
   121  func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption) *VCDClient {
   122  	minVcdApiVersion := "37.0" // supported by 10.4+
   123  	userDefinedApiVersion := os.Getenv("GOVCD_API_VERSION")
   124  	if userDefinedApiVersion != "" {
   125  		_, err := semver.NewVersion(userDefinedApiVersion)
   126  		if err != nil {
   127  			// We do not have error in return of this function signature.
   128  			// To avoid breaking API the only thing we can do is panic.
   129  			panic(fmt.Sprintf("unable to initialize VCD client from environment variable GOVCD_API_VERSION. Version '%s' is not valid: %s", userDefinedApiVersion, err))
   130  		}
   131  		minVcdApiVersion = userDefinedApiVersion
   132  	}
   133  
   134  	// Setting defaults
   135  	// #nosec G402 -- InsecureSkipVerify: insecure - This allows connecting to VCDs with self-signed certificates
   136  	vcdClient := &VCDClient{
   137  		Client: Client{
   138  			APIVersion: minVcdApiVersion,
   139  			// UserAgent cannot embed exact version by default because this is source code and is supposed to be used by programs,
   140  			// but any client can customize or disable it at all using WithHttpUserAgent() configuration options function.
   141  			UserAgent: "go-vcloud-director",
   142  			VCDHREF:   vcdEndpoint,
   143  			Http: http.Client{
   144  				Transport: &http.Transport{
   145  					TLSClientConfig: &tls.Config{
   146  						InsecureSkipVerify: insecure,
   147  					},
   148  					Proxy:               http.ProxyFromEnvironment,
   149  					TLSHandshakeTimeout: 120 * time.Second, // Default timeout for TSL hand shake
   150  				},
   151  				Timeout: 600 * time.Second, // Default value for http request+response timeout
   152  			},
   153  			MaxRetryTimeout: 60, // Default timeout in seconds for retries calls in functions
   154  		},
   155  	}
   156  
   157  	// Override defaults with functional options
   158  	for _, option := range options {
   159  		err := option(vcdClient)
   160  		if err != nil {
   161  			// We do not have error in return of this function signature.
   162  			// To avoid breaking API the only thing we can do is panic.
   163  			panic(fmt.Sprintf("unable to initialize VCD client: %s", err))
   164  		}
   165  	}
   166  	return vcdClient
   167  }
   168  
   169  // Authenticate is a helper function that performs a login in VMware Cloud Director.
   170  func (vcdClient *VCDClient) Authenticate(username, password, org string) error {
   171  	_, err := vcdClient.GetAuthResponse(username, password, org)
   172  	return err
   173  }
   174  
   175  // GetAuthResponse performs authentication and returns the full HTTP response
   176  // The purpose of this function is to preserve information that is useful
   177  // for token-based authentication
   178  func (vcdClient *VCDClient) GetAuthResponse(username, password, org string) (*http.Response, error) {
   179  	// LoginUrl
   180  	err := vcdClient.vcdloginurl()
   181  	if err != nil {
   182  		return nil, fmt.Errorf("error finding LoginUrl: %s", err)
   183  	}
   184  
   185  	// Choose correct auth mechanism based on what type of authentication is used. The end result
   186  	// for each of the below functions is to set authorization token vcdCli.Client.VCDToken.
   187  	var resp *http.Response
   188  	switch {
   189  	case vcdClient.Client.UseSamlAdfs:
   190  		err = vcdClient.authorizeSamlAdfs(username, password, org, vcdClient.Client.CustomAdfsRptId)
   191  		if err != nil {
   192  			return nil, fmt.Errorf("error authorizing SAML: %s", err)
   193  		}
   194  	default:
   195  		// Authorize
   196  		resp, err = vcdClient.vcdCloudApiAuthorize(username, password, org)
   197  		if err != nil {
   198  			return nil, fmt.Errorf("error authorizing: %s", err)
   199  		}
   200  	}
   201  
   202  	vcdClient.LogSessionInfo()
   203  	return resp, nil
   204  }
   205  
   206  // SetToken will set the authorization token in the client, without using other credentials
   207  // Up to version 29, token authorization uses the header key x-vcloud-authorization
   208  // In version 30+ it also uses X-Vmware-Vcloud-Access-Token:TOKEN coupled with
   209  // X-Vmware-Vcloud-Token-Type:"bearer"
   210  func (vcdClient *VCDClient) SetToken(org, authHeader, token string) error {
   211  	if authHeader == ApiTokenHeader {
   212  		util.Logger.Printf("[DEBUG] Attempt authentication using API token")
   213  		apiToken, err := vcdClient.GetBearerTokenFromApiToken(org, token)
   214  		if err != nil {
   215  			util.Logger.Printf("[DEBUG] Authentication using API token was UNSUCCESSFUL: %s", err)
   216  			return err
   217  		}
   218  		token = apiToken.AccessToken
   219  		authHeader = BearerTokenHeader
   220  		vcdClient.Client.UsingAccessToken = true
   221  		util.Logger.Printf("[DEBUG] Authentication using API token was SUCCESSFUL")
   222  	}
   223  	if !vcdClient.Client.UsingAccessToken {
   224  		vcdClient.Client.UsingBearerToken = true
   225  	}
   226  	vcdClient.Client.VCDAuthHeader = authHeader
   227  	vcdClient.Client.VCDToken = token
   228  
   229  	err := vcdClient.vcdloginurl()
   230  	if err != nil {
   231  		return fmt.Errorf("error finding LoginUrl: %s", err)
   232  	}
   233  
   234  	vcdClient.Client.IsSysAdmin = strings.EqualFold(org, "system")
   235  	// Get query href
   236  	vcdClient.QueryHREF = vcdClient.Client.VCDHREF
   237  	vcdClient.QueryHREF.Path += "/query"
   238  
   239  	// The client is now ready to connect using the token, but has not communicated with the vCD yet.
   240  	// To make sure that it is working, we run a request for the org list.
   241  	// This list should work always: when run as system administrator, it retrieves all organizations.
   242  	// When run as org user, it only returns the organization the user is authorized to.
   243  	// In both cases, we discard the list, as we only use it to certify that the token works.
   244  	orgListHREF := vcdClient.Client.VCDHREF
   245  	orgListHREF.Path += "/org"
   246  
   247  	orgList := new(types.OrgList)
   248  
   249  	_, err = vcdClient.Client.ExecuteRequest(orgListHREF.String(), http.MethodGet,
   250  		"", "error connecting to vCD using token: %s", nil, orgList)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	vcdClient.LogSessionInfo()
   255  	return nil
   256  }
   257  
   258  // Disconnect performs a disconnection from the VMware Cloud Director API endpoint.
   259  func (vcdClient *VCDClient) Disconnect() error {
   260  	if vcdClient.Client.VCDToken == "" && vcdClient.Client.VCDAuthHeader == "" {
   261  		return fmt.Errorf("cannot disconnect, client is not authenticated")
   262  	}
   263  	req := vcdClient.Client.NewRequest(map[string]string{}, http.MethodDelete, vcdClient.sessionHREF, nil)
   264  	// Add the Accept header for vCA
   265  	req.Header.Add("Accept", "application/xml;version="+vcdClient.Client.APIVersion)
   266  	// Set Authorization Header
   267  	req.Header.Add(vcdClient.Client.VCDAuthHeader, vcdClient.Client.VCDToken)
   268  	if _, err := checkResp(vcdClient.Client.Http.Do(req)); err != nil {
   269  		return fmt.Errorf("error processing session delete for VMware Cloud Director: %s", err)
   270  	}
   271  	return nil
   272  }
   273  
   274  // WithMaxRetryTimeout allows default vCDClient MaxRetryTimeout value override
   275  func WithMaxRetryTimeout(timeoutSeconds int) VCDClientOption {
   276  	return func(vcdClient *VCDClient) error {
   277  		vcdClient.Client.MaxRetryTimeout = timeoutSeconds
   278  		return nil
   279  	}
   280  }
   281  
   282  // WithAPIVersion allows to override default API version. Please be cautious
   283  // about changing the version as the default specified is the most tested.
   284  func WithAPIVersion(version string) VCDClientOption {
   285  	return func(vcdClient *VCDClient) error {
   286  		vcdClient.Client.APIVersion = version
   287  		return nil
   288  	}
   289  }
   290  
   291  // WithHttpTimeout allows to override default http timeout
   292  func WithHttpTimeout(timeout int64) VCDClientOption {
   293  	return func(vcdClient *VCDClient) error {
   294  		vcdClient.Client.Http.Timeout = time.Duration(timeout) * time.Second
   295  		return nil
   296  	}
   297  }
   298  
   299  // WithSamlAdfs specifies if SAML auth is used for authenticating to vCD instead of local login.
   300  // The following conditions must be met so that SAML authentication works:
   301  // * SAML IdP (Identity Provider) is Active Directory Federation Service (ADFS)
   302  // * WS-Trust authentication endpoint "/adfs/services/trust/13/usernamemixed" must be enabled on
   303  // ADFS server
   304  // By default vCD SAML Entity ID will be used as Relaying Party Trust Identifier unless
   305  // customAdfsRptId is specified
   306  func WithSamlAdfs(useSaml bool, customAdfsRptId string) VCDClientOption {
   307  	return func(vcdClient *VCDClient) error {
   308  		vcdClient.Client.UseSamlAdfs = useSaml
   309  		vcdClient.Client.CustomAdfsRptId = customAdfsRptId
   310  		return nil
   311  	}
   312  }
   313  
   314  // WithHttpUserAgent allows to specify HTTP user-agent which can be useful for statistics tracking.
   315  // By default User-Agent is set to "go-vcloud-director". It can be unset by supplying an empty value.
   316  func WithHttpUserAgent(userAgent string) VCDClientOption {
   317  	return func(vcdClient *VCDClient) error {
   318  		vcdClient.Client.UserAgent = userAgent
   319  		return nil
   320  	}
   321  }
   322  
   323  // WithHttpHeader allows to specify custom HTTP header values.
   324  // Typical usage of this function is to inject a tenant context into the client.
   325  //
   326  // WARNING: Using this function in an environment with concurrent operations may result in negative side effects,
   327  // such as operations as system administrator and as tenant using the same client.
   328  // This setting is justified when we want to start a session where the additional header is always needed.
   329  // For cases where we need system administrator and tenant operations in the same environment we can either
   330  // a) use two separate clients
   331  // or b) use the `additionalHeader` parameter in *newRequest* functions
   332  func WithHttpHeader(options map[string]string) VCDClientOption {
   333  	return func(vcdClient *VCDClient) error {
   334  		vcdClient.Client.customHeader = make(http.Header)
   335  		for k, v := range options {
   336  			vcdClient.Client.customHeader.Add(k, v)
   337  		}
   338  		return nil
   339  	}
   340  }
   341  
   342  // WithIgnoredMetadata allows specifying metadata entries to be ignored when using metadata_v2 methods.
   343  // It can be unset by supplying an empty value.
   344  // See the documentation of the IgnoredMetadata structure for more information.
   345  func WithIgnoredMetadata(ignoredMetadata []IgnoredMetadata) VCDClientOption {
   346  	return func(vcdClient *VCDClient) error {
   347  		vcdClient.Client.IgnoredMetadata = ignoredMetadata
   348  		return nil
   349  	}
   350  }