Skip to content

Commit

Permalink
Add VMC Oauth app support (vmware#1080)
Browse files Browse the repository at this point in the history
This change adds support of NSX token exchange on VMC using Oauth app,
which is a newer method compared to using refresh token.

Signed-off-by: Shawn Wang <[email protected]>
  • Loading branch information
wsquan171 authored Jan 23, 2024
1 parent e4a68a6 commit f1e5494
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 39 deletions.
145 changes: 108 additions & 37 deletions nsxt/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,30 @@ func Provider() *schema.Provider {
Type: schema.TypeString,
Optional: true,
Description: "URL for VMC authorization service (CSP)",
DefaultFunc: schema.EnvDefaultFunc("NSXT_VMC_AUTH_HOST", "console.cloud.vmware.com/csp/gateway/am/api/auth/api-tokens/authorize"),
DefaultFunc: schema.EnvDefaultFunc("NSXT_VMC_AUTH_HOST", nil),
},
"vmc_token": {
Type: schema.TypeString,
Optional: true,
Description: "Long-living API token for VMC authorization",
DefaultFunc: schema.EnvDefaultFunc("NSXT_VMC_TOKEN", nil),
Type: schema.TypeString,
Optional: true,
Description: "Long-living API token for VMC authorization",
DefaultFunc: schema.EnvDefaultFunc("NSXT_VMC_TOKEN", nil),
ConflictsWith: []string{"vmc_client_id", "vmc_client_secret"},
},
"vmc_client_id": {
Type: schema.TypeString,
Optional: true,
Description: "ID of OAuth App associated with the VMC organization",
DefaultFunc: schema.EnvDefaultFunc("NSXT_VMC_CLIENT_ID", nil),
ConflictsWith: []string{"vmc_token"},
RequiredWith: []string{"vmc_client_secret"},
},
"vmc_client_secret": {
Type: schema.TypeString,
Optional: true,
Description: "Secret of OAuth App associated with the VMC organization",
DefaultFunc: schema.EnvDefaultFunc("NSXT_VMC_CLIENT_SECRET", nil),
ConflictsWith: []string{"vmc_token"},
RequiredWith: []string{"vmc_client_id"},
},
"vmc_auth_mode": {
Type: schema.TypeString,
Expand All @@ -189,7 +206,7 @@ func Provider() *schema.Provider {
Type: schema.TypeList,
Optional: true,
Description: "license keys",
ConflictsWith: []string{"vmc_token"},
ConflictsWith: []string{"vmc_token", "vmc_client_id", "vmc_client_secret"},
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringMatch(
Expand Down Expand Up @@ -456,21 +473,37 @@ func Provider() *schema.Provider {
}
}

func isVMCCredentialSet(d *schema.ResourceData) bool {
// Refresh token
vmcToken := d.Get("vmc_token").(string)
if len(vmcToken) > 0 {
return true
}

// Oauth app
vmcClientID := d.Get("vmc_client_id").(string)
vmcClientSecret := d.Get("vmc_client_secret").(string)
if len(vmcClientSecret) > 0 && len(vmcClientID) > 0 {
return true
}

return false
}

func configureNsxtClient(d *schema.ResourceData, clients *nsxtClients) error {
onDemandConn := d.Get("on_demand_connection").(bool)
clientAuthCertFile := d.Get("client_auth_cert_file").(string)
clientAuthKeyFile := d.Get("client_auth_key_file").(string)
clientAuthCert := d.Get("client_auth_cert").(string)
clientAuthKey := d.Get("client_auth_key").(string)
vmcToken := d.Get("vmc_token").(string)
vmcAuthMode := d.Get("vmc_auth_mode").(string)

if onDemandConn {
// On demand connection option is not supported with old SDK
return nil
}

if (len(vmcToken) > 0) || (vmcAuthMode == "Basic") {
if (vmcAuthMode == "Basic") || isVMCCredentialSet(d) {
// VMC can operate without token with basic auth, however MP API is not
// available for cloud admin user
return nil
Expand Down Expand Up @@ -564,21 +597,65 @@ type jwtToken struct {
RefreshToken string `json:"refresh_token"`
}

func getAPIToken(vmcAuthHost string, vmcAccessToken string) (string, error) {
type vmcAuthInfo struct {
authHost string
authMode string
accessToken string
clientID string
clientSecret string
}

func getVmcAuthInfo(d *schema.ResourceData) *vmcAuthInfo {
vmcInfo := vmcAuthInfo{
authHost: d.Get("vmc_auth_host").(string),
authMode: d.Get("vmc_auth_mode").(string),
accessToken: d.Get("vmc_token").(string),
clientID: d.Get("vmc_client_id").(string),
clientSecret: d.Get("vmc_client_secret").(string),
}
if len(vmcInfo.authHost) > 0 {
return &vmcInfo
}

// Fill in default auth host + url based on auth method
if len(vmcInfo.accessToken) > 0 {
vmcInfo.authHost = "console.cloud.vmware.com/csp/gateway/am/api/auth/api-tokens/authorize"
} else if len(vmcInfo.clientSecret) > 0 && len(vmcInfo.clientID) > 0 {
vmcInfo.authHost = "console.cloud.vmware.com/csp/gateway/am/api/auth/authorize"
}
return &vmcInfo
}

func (v *vmcAuthInfo) IsZero() bool {
return len(v.accessToken) == 0 && len(v.clientID) == 0 && len(v.clientSecret) == 0
}

func (v *vmcAuthInfo) getAPIToken() (string, error) {
var req *http.Request

payload := strings.NewReader("refresh_token=" + vmcAccessToken)
req, _ := http.NewRequest("POST", "https://"+vmcAuthHost, payload)
// Access token
if len(v.accessToken) > 0 {
payload := strings.NewReader("refresh_token=" + v.accessToken)
req, _ = http.NewRequest("POST", "https://"+v.authHost, payload)
}
// Oauth app
if len(v.clientSecret) > 0 && len(v.clientID) > 0 {
payload := strings.NewReader("grant_type=client_credentials")
req, _ = http.NewRequest("POST", "https://"+v.authHost, payload)
req.SetBasicAuth(v.clientID, v.clientSecret)
}
if req == nil {
return "", fmt.Errorf("invalid VMC auth input")
}

req.Header.Add("content-type", "application/x-www-form-urlencoded")
res, err := http.DefaultClient.Do(req)

if err != nil {
return "", err
}

if res.StatusCode != 200 {
b, _ := ioutil.ReadAll(res.Body)
return "", fmt.Errorf("Unexpected status code %d trying to get auth token. %s", res.StatusCode, string(b))
return "", fmt.Errorf("unexpected status code %d trying to get auth token. %s", res.StatusCode, string(b))
}

defer res.Body.Close()
Expand Down Expand Up @@ -665,17 +742,15 @@ func configurePolicyConnectorData(d *schema.ResourceData, clients *nsxtClients)
host := d.Get("host").(string)
username := d.Get("username").(string)
password := d.Get("password").(string)
vmcAccessToken := d.Get("vmc_token").(string)
vmcAuthHost := d.Get("vmc_auth_host").(string)
clientAuthCertFile := d.Get("client_auth_cert_file").(string)
clientAuthCert := d.Get("client_auth_cert").(string)
clientAuthDefined := (len(clientAuthCertFile) > 0) || (len(clientAuthCert) > 0)
policyEnforcementPoint := d.Get("enforcement_point").(string)
policyGlobalManager := d.Get("global_manager").(bool)
vmcAuthMode := d.Get("vmc_auth_mode").(string)
vmcInfo := getVmcAuthInfo(d)

isVMC := false
if (len(vmcAccessToken) > 0) || (vmcAuthMode == "Basic") {
if (vmcInfo.authMode == "Basic") || isVMCCredentialSet(d) {
isVMC = true
if onDemandConn {
return fmt.Errorf("on demand connection option is not supported with VMC")
Expand All @@ -695,7 +770,7 @@ func configurePolicyConnectorData(d *schema.ResourceData, clients *nsxtClients)
securityContextNeeded = false
}
if securityContextNeeded {
securityCtx, err := getConfiguredSecurityContext(clients, vmcAccessToken, vmcAuthHost, vmcAuthMode, username, password)
securityCtx, err := getConfiguredSecurityContext(clients, vmcInfo, username, password)
if err != nil {
return err
}
Expand Down Expand Up @@ -740,37 +815,33 @@ func configurePolicyConnectorData(d *schema.ResourceData, clients *nsxtClients)
return err
}

func getConfiguredSecurityContext(clients *nsxtClients, vmcAccessToken string, vmcAuthHost string, vmcAuthMode string, username string, password string) (*core.SecurityContextImpl, error) {
func getConfiguredSecurityContext(clients *nsxtClients, vmcInfo *vmcAuthInfo, username string, password string) (*core.SecurityContextImpl, error) {
securityCtx := core.NewSecurityContextImpl()
if len(vmcAccessToken) > 0 {
if vmcAuthHost == "" {
return nil, fmt.Errorf("vmc auth host must be provided if auth token is provided")
if vmcInfo == nil || vmcInfo.IsZero() {
if username == "" {
return nil, fmt.Errorf("username must be provided")
}

apiToken, err := getAPIToken(vmcAuthHost, vmcAccessToken)
if password == "" {
return nil, fmt.Errorf("password must be provided")
}

securityCtx.SetProperty(security.AUTHENTICATION_SCHEME_ID, security.USER_PASSWORD_SCHEME_ID)
securityCtx.SetProperty(security.USER_KEY, username)
securityCtx.SetProperty(security.PASSWORD_KEY, password)
} else {
apiToken, err := vmcInfo.getAPIToken()
if err != nil {
return nil, err
}

// We'll be sending Bearer token anyway even with scp-auth-token auth
// For now, node API is not working on VMC without Bearer token present
clients.CommonConfig.BearerToken = apiToken
if vmcAuthMode != "Bearer" {
if vmcInfo.authMode != "Bearer" {
securityCtx.SetProperty(security.AUTHENTICATION_SCHEME_ID, security.OAUTH_SCHEME_ID)
securityCtx.SetProperty(security.ACCESS_TOKEN, apiToken)
}
} else {
if username == "" {
return nil, fmt.Errorf("username must be provided")
}

if password == "" {
return nil, fmt.Errorf("password must be provided")
}

securityCtx.SetProperty(security.AUTHENTICATION_SCHEME_ID, security.USER_PASSWORD_SCHEME_ID)
securityCtx.SetProperty(security.USER_KEY, username)
securityCtx.SetProperty(security.PASSWORD_KEY, password)
}
return securityCtx, nil
}
Expand Down
2 changes: 1 addition & 1 deletion nsxt/resource_nsxt_manager_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ func getNewNsxtClient(node NsxClusterNode, d *schema.ResourceData, clients inter

func configureNewClient(newClient *nsxtClients, oldClient *nsxtClients, host string, username string, password string) error {
newClient.Host = host
securityCtx, err := getConfiguredSecurityContext(newClient, "", "", "", username, password)
securityCtx, err := getConfiguredSecurityContext(newClient, &vmcAuthInfo{}, username, password)
if err != nil {
return fmt.Errorf("Failed to configure new client with host %s: %s", host, err)
}
Expand Down
10 changes: 9 additions & 1 deletion website/docs/index.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,15 @@ The following arguments are used to configure the VMware NSX-T Provider:
partially successful realization as valid state and not fail apply.
* `vmc_token` - (Optional) Long-lived API token for authenticating with VMware
Cloud Services APIs. This token will be used to short-lived token that is
needed to communicate with NSX Manager in VMC environment.
needed to communicate with NSX Manager in VMC environment. Can not be specified
together with `vmc_client_id` and `vmc_client_secret`.
Note that only subset of policy resources are supported with VMC environment.
* `vmc_client_id`- (Optional) ID of OAuth App associated with the VMC organization.
The combination with `vmc_client_secret` is used to authenticate when calling
VMware Cloud Services APIs. Can not be specified together with `vmc_token`.
* `vmc_client_secret` - (Optional) Secret of OAuth App associated with the VMC
organization. The combination with `vmc_client_id` is used to authenticate when
calling VMware Cloud Services APIs. Can not be specified together with `vmc_token`.
Note that only subset of policy resources are supported with VMC environment.
* `vmc_auth_host` - (Optional) URL for VMC authorization service that is used
to obtain short-lived token for NSX manager access. Defaults to VMC
Expand Down

0 comments on commit f1e5494

Please sign in to comment.