From 78c1a643607c2fb40e5098aa585c538f8a5962ce Mon Sep 17 00:00:00 2001 From: Ferran Rodenas Date: Thu, 12 Dec 2024 13:38:17 -0800 Subject: [PATCH] Add support for VCF Automation authentication Signed-off-by: Ferran Rodenas --- README.md | 2 +- vra/client.go | 130 +++++++++++++++++++++++++++++-- vra/provider.go | 13 +++- website/docs/index.html.markdown | 1 + 4 files changed, 138 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 90d02f8..357cdfb 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ terraform init -upgrade ## Contributing -The Terraform Provider for VMware Cloud Foundation is the work of many contributors and the project team appreciates your help! +The Terraform Provider for VMware Aria Automation is the work of many contributors and the project team appreciates your help! If you discover a bug or would like to suggest an enhancement, submit [an issue][provider-issues]. diff --git a/vra/client.go b/vra/client.go index cf2b63e..b824140 100644 --- a/vra/client.go +++ b/vra/client.go @@ -5,10 +5,15 @@ package vra import ( + "encoding/base64" + "encoding/json" + "errors" "fmt" + "io" "log" "net/http" neturl "net/url" + "regexp" "strings" "sync" "time" @@ -16,6 +21,7 @@ import ( "github.com/go-openapi/runtime" httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/vmware/vra-sdk-go/pkg/client" "github.com/vmware/vra-sdk-go/pkg/client/login" @@ -69,6 +75,7 @@ func (t *ReauthTimeout) ShouldReload() bool { type ReauthorizeRuntime struct { origClient httptransport.Runtime url string + organization string refreshToken string insecure bool reauthtimer *ReauthTimeout @@ -78,7 +85,7 @@ type ReauthorizeRuntime struct { func (r *ReauthorizeRuntime) Submit(operation *runtime.ClientOperation) (interface{}, error) { if r.reauthtimer.ShouldReload() { log.Printf("Reauthorize timer expired, generating a new access token") - token, tokenErr := getToken(r.url, r.refreshToken, r.insecure) + token, tokenErr := getToken(r.url, r.organization, r.refreshToken, r.insecure) if tokenErr != nil { return nil, tokenErr } @@ -99,7 +106,7 @@ func (r *ReauthorizeRuntime) Submit(operation *runtime.ClientOperation) (interfa // We have a 401 with a refresh token, let's try refreshing once and try again log.Printf("Response back was a 401, trying again with new access token") - token, tokenErr := getToken(r.url, r.refreshToken, r.insecure) + token, tokenErr := getToken(r.url, r.organization, r.refreshToken, r.insecure) if tokenErr != nil { return result, err } @@ -117,8 +124,8 @@ type Client struct { } // NewClientFromRefreshToken configures and returns a VRA "Client" struct using "refresh_token" from provider config -func NewClientFromRefreshToken(url, refreshToken string, insecure bool, reauth string, apiTimeout int) (interface{}, error) { - token, err := getToken(url, refreshToken, insecure) +func NewClientFromRefreshToken(url, organization, refreshToken string, insecure bool, reauth string, apiTimeout int) (interface{}, error) { + token, err := getToken(url, organization, refreshToken, insecure) if err != nil { return "", err } @@ -133,7 +140,7 @@ func NewClientFromRefreshToken(url, refreshToken string, insecure bool, reauth s if err != nil { return "", err } - apiClient.SetTransport(&ReauthorizeRuntime{*t, url, refreshToken, insecure, InitializeTimeout(reautDuration)}) + apiClient.SetTransport(&ReauthorizeRuntime{*t, url, organization, refreshToken, insecure, InitializeTimeout(reautDuration)}) return &Client{url, apiClient}, nil } @@ -147,7 +154,75 @@ func NewClientFromAccessToken(url, accessToken string, insecure bool, apiTimeout return &Client{url, apiClient}, nil } -func getToken(url, refreshToken string, insecure bool) (string, error) { +func getToken(url, organization, refreshToken string, insecure bool) (string, error) { + isVCFA, err := isVCFA(url, insecure) + if err != nil { + return "", fmt.Errorf("error determining whether vRA or VCFA: %s", err) + } + if isVCFA { + if organization == "" { + return "", errors.New("organization is required for VCFA") + } else if organization == "system" { + return "", errors.New("system organization is not allowed") + } + return getVCFAToken(url, organization, refreshToken, insecure) + } + return getVRAToken(url, refreshToken, insecure) +} + +func isVCFA(url string, insecure bool) (bool, error) { + parsedURL, err := neturl.Parse(url) + if err != nil { + return false, fmt.Errorf("error parsing the URL %s: %s", url, err) + } + transport, err := createTransport(insecure) + if err != nil { + return false, fmt.Errorf("error creating an http transport: %s", err) + } + client := &http.Client{Transport: transport} + response, err := client.Get(fmt.Sprintf("%s://%s/config.json", parsedURL.Scheme, parsedURL.Host)) + if err != nil { + return false, fmt.Errorf("error retrieving the configuration from url %s: %s", url, err) + } + if response.StatusCode != http.StatusOK { + if response.StatusCode == http.StatusNotFound { + return false, nil + } + return false, fmt.Errorf("error retrieving the configuration from url %s, http response code is %d", url, response.StatusCode) + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return false, fmt.Errorf("error reading the http response body from url %s: %s", url, err) + } + var config map[string]interface{} + if err := json.Unmarshal(body, &config); err != nil { + return false, fmt.Errorf("error unmarshalling the configuration from url %s: %s", url, err) + } + applicationVersion, ok := config["applicationVersion"] + if ok { + productName, err := base64.StdEncoding.DecodeString(applicationVersion.(string)) + if err != nil { + return false, fmt.Errorf("error decoding the application version %s: %s", applicationVersion, err) + } + re := regexp.MustCompile(`v?(\d+\.\d+\.\d+\.\d+)`) + match := re.FindStringSubmatch(string(productName)) + if match != nil { + productVersion, err := version.NewVersion(match[1]) + if err != nil { + return false, fmt.Errorf("error parsing the application version %s: %s", match[1], err) + } + vcfaVersion, _ := version.NewVersion("9.0.0.0") + if productVersion.GreaterThanOrEqual(vcfaVersion) { + return true, nil + } + } + } + return false, nil +} + +// Retrieve the access token for vRA 8.x instances +func getVRAToken(url, refreshToken string, insecure bool) (string, error) { parsedURL, err := neturl.Parse(url) if err != nil { return "", err @@ -173,6 +248,49 @@ func getToken(url, refreshToken string, insecure bool) (string, error) { return *authTokenResponse.Payload.Token, nil } +// Retrieve the access token for VCFA 9.x instances +func getVCFAToken(url, org string, refreshToken string, insecure bool) (string, error) { + parsedURL, err := neturl.Parse(url) + if err != nil { + return "", fmt.Errorf("error parsing the URL %s: %s", url, err) + } + transport, err := createTransport(insecure) + if err != nil { + return "", fmt.Errorf("error creating an http transport: %s", err) + } + client := &http.Client{Transport: transport} + + data := neturl.Values{} + data.Set("grant_type", "refresh_token") + data.Set("refresh_token", refreshToken) + + tenant := "tenant/" + org + if strings.EqualFold(org, "system") { + tenant = "provider" + } + response, err := client.Post(fmt.Sprintf("%s://%s/tm/oauth/%s/token", parsedURL.Scheme, parsedURL.Host, tenant), "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) + if err != nil { + return "", fmt.Errorf("error retrieving the auth token: %s", err) + } + if response.StatusCode != http.StatusOK { + return "", fmt.Errorf("error retrieving the auth token, http response code is %d", response.StatusCode) + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return "", fmt.Errorf("error reading the http response body: %s", err) + } + var tokenData map[string]interface{} + if err := json.Unmarshal(body, &tokenData); err != nil { + return "", fmt.Errorf("error unmarshalling the token data: %s", err) + } + accessToken, ok := tokenData["access_token"] + if ok { + return accessToken.(string), nil + } + return "", errors.New("Unable to obtain an access token") +} + // SwaggerLogger is the interface into the swagger logging facility which logs http traffic type SwaggerLogger struct{} diff --git a/vra/provider.go b/vra/provider.go index b6c7a86..d87f856 100644 --- a/vra/provider.go +++ b/vra/provider.go @@ -22,6 +22,12 @@ func Provider() *schema.Provider { DefaultFunc: schema.EnvDefaultFunc("VRA_URL", nil), Description: "The base url for API operations.", }, + "organization": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("VCFA_ORGANIZATION", nil), + Description: "Organization name (required for VCF Automation).", + }, "refresh_token": { Type: schema.TypeString, Optional: true, @@ -167,11 +173,16 @@ func Provider() *schema.Provider { func configureProvider(d *schema.ResourceData) (interface{}, error) { url := d.Get("url").(string) + organization := "" refreshToken := "" accessToken := "" reauth := "0" apiTimeout := 0 + if v, ok := d.GetOk("organization"); ok { + organization = v.(string) + } + if v, ok := d.GetOk("refresh_token"); ok { refreshToken = v.(string) } @@ -198,5 +209,5 @@ func configureProvider(d *schema.ResourceData) (interface{}, error) { return NewClientFromAccessToken(url, accessToken, insecure, apiTimeout) } - return NewClientFromRefreshToken(url, refreshToken, insecure, reauth, apiTimeout) + return NewClientFromRefreshToken(url, organization, refreshToken, insecure, reauth, apiTimeout) } diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index e964a36..11ed37d 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -66,6 +66,7 @@ Note that in all of the examples you will need to update attributes - such as `u The following arguments are used to configure the Terraform Provider for VMware Aria Automation: - `url` - (Required) This is the URL to the VMware Aria Automation endpoint. Can also be specified with the `VRA_URL` environment variable. +- `organization` - (Optional) The name of the organization. Required when using VCF Automation, otherwise, this parameter is ignored. Can also be specified with the `VCFA_ORGANIZATION` environment variable. - `access_token` - (Optional) This is the access token used to create an API refresh token. Can also be specified with the `VRA_ACCESS_TOKEN` environment variable. - `refresh_token` - (Optional) This is a refresh token used for API access that has been pre-generated. One of `access_token` or `refresh_token` is required. Can also be specified with the `VRA_REFRESH_TOKEN` environment variable. - `insecure` - (Optional) This specifies whether if the TLS certificates are validated. Can also be specified with the `VRA_INSECURE` environment variable.