Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VMC Oauth app support #1080

Merged
merged 4 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"),
wsquan171 marked this conversation as resolved.
Show resolved Hide resolved
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