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 }