Skip to content

Commit

Permalink
Merge pull request #173 from dimitarproynov/main
Browse files Browse the repository at this point in the history
Add support for OAuth2.0 server-to-server authentication
  • Loading branch information
dimitarproynov authored Feb 22, 2023
2 parents 566f7db + 8dc5030 commit fe9664e
Show file tree
Hide file tree
Showing 18 changed files with 241 additions and 144 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/acceptance_tests_lite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ jobs:
- run: make testacc TESTARGS="-run='TestAccResourceVmcSddcZerocloud|TestAccResourceVmcClusterZerocloud|TestAccResourceVmcSiteRecoveryZerocloud|TestAccResourceVmcSrmNodeZerocloud|TestAccDataSourceVmcCustomerSubnetsBasic|TestAccDataSourceVmcConnectedAccountsBasic|TestAccDataSourceVmcOrgBasic|TestAccDataSourceVmcSddcBasic|TestAccResourceSddcGroupZerocloud' -parallel 4"
env:
TF_ACC: '1'
API_TOKEN: ${{ secrets.API_TOKEN }}
CLIENT_ID: ${{ secrets.CLIENT_ID }}
CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
AWS_ACCOUNT_NUMBER: ${{ secrets.AWS_ACCOUNT_NUMBER }}
CSP_URL: ${{ secrets.CSP_URL }}
ORG_DISPLAY_NAME: ${{ secrets.ORG_DISPLAY_NAME }}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/vmware/vsphere-automation-sdk-go/services/vmc v0.10.0
github.com/vmware/vsphere-automation-sdk-go/services/vmc/autoscaler v0.4.0
github.com/vmware/vsphere-automation-sdk-go/services/vmc/draas v0.4.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
)

require (
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
99 changes: 85 additions & 14 deletions vmc/connector/clientconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package connector

import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/vmware/terraform-provider-vmc/vmc/constants"
"golang.org/x/oauth2/clientcredentials"
"io"
"net/http"
"reflect"
Expand All @@ -26,23 +28,39 @@ type Authenticator interface {
type Wrapper struct {
client.Connector
RefreshToken string
ClientID string
ClientSecret string
OrgID string
VmcURL string
CspURL string
}

func CopyWrapper(original Wrapper) *Wrapper {
return &original
}

func (c *Wrapper) Authenticate() error {
var err error
httpClient := http.Client{}
c.Connector, err = NewClientConnectorByRefreshToken(c.RefreshToken, c.VmcURL, c.CspURL, httpClient)
if err != nil {
return err
if len(c.RefreshToken) > 0 {
c.Connector, err = newClientConnectorByRefreshToken(c.RefreshToken, c.VmcURL, c.CspURL, httpClient)
if err != nil {
return err
}
return nil
}
if len(c.ClientID) > 0 && len(c.ClientSecret) > 0 {
c.Connector, err = newClientConnectorByClientID(c.ClientID, c.ClientSecret, c.VmcURL, c.CspURL, httpClient)
if err != nil {
return err
}
return nil
}
return nil
return fmt.Errorf("no refreshToken or ClientID/ClientSecret provided")
}

// NewClientConnectorByRefreshToken returns client connector to any VMC service by using OAuth authentication using Refresh Token.
func NewClientConnectorByRefreshToken(refreshToken, serviceURL, cspURL string,
// newClientConnectorByRefreshToken returns client connector to any VMC service by using OAuth authentication using Refresh Token.
func newClientConnectorByRefreshToken(refreshToken, serviceURL, cspURL string,
httpClient http.Client) (client.Connector, error) {

if len(serviceURL) <= 0 {
Expand All @@ -57,7 +75,7 @@ func NewClientConnectorByRefreshToken(refreshToken, serviceURL, cspURL string,
constants.CspRefreshURLSuffix
}

securityCtx, err := SecurityContextByRefreshToken(refreshToken, cspURL)
securityCtx, err := securityContextByRefreshToken(refreshToken, cspURL)
if err != nil {
return nil, err
}
Expand All @@ -69,7 +87,7 @@ func NewClientConnectorByRefreshToken(refreshToken, serviceURL, cspURL string,
}

// SecurityContextByRefreshToken returns Security Context with access token that is received from Cloud Service Provider using Refresh Token by OAuth authentication scheme.
func SecurityContextByRefreshToken(refreshToken string, cspURL string) (core.SecurityContext, error) {
func securityContextByRefreshToken(refreshToken string, cspURL string) (core.SecurityContext, error) {
payload := strings.NewReader("refresh_token=" + refreshToken)

req, _ := http.NewRequest("POST", cspURL, payload)
Expand All @@ -82,15 +100,68 @@ func SecurityContextByRefreshToken(refreshToken string, cspURL string) (core.Sec
return nil, err
}

if res.StatusCode != 200 {
b, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("response from Cloud Service Provider contains status code %d : %s", res.StatusCode, string(b))
securityCtx, err := parseAuthnResponse(res)
if err != nil {
return nil, err
}
return securityCtx, nil
}

defer res.Body.Close()
// newClientConnectorByClientID returns client connector to any VMC service by using OAuth authentication using clientId and secret.
func newClientConnectorByClientID(clientID, clientSecret, serviceURL, cspURL string,
httpClient http.Client) (client.Connector, error) {

if len(serviceURL) <= 0 {
serviceURL = constants.DefaultVmcURL
}

if len(cspURL) <= 0 {
cspURL = constants.DefaultCspURL +
constants.CspTokenURLSuffix
} else {
cspURL = cspURL +
constants.CspTokenURLSuffix
}

securityCtx, err := securityContextByClientID(clientID, clientSecret, cspURL)
if err != nil {
return nil, err
}

connector := client.NewRestConnector(serviceURL, httpClient)
connector.SetSecurityContext(securityCtx)

return connector, nil
}

func securityContextByClientID(clientID string, clientSecret string, cspTokenEndpointURL string) (core.SecurityContext, error) {
oauth2Config := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: cspTokenEndpointURL,
}
token, err := oauth2Config.Token(context.TODO())
if err != nil {
return nil, err
}
return security.NewOauthSecurityContext(token.AccessToken), nil
}

func parseAuthnResponse(response *http.Response) (*security.OauthSecurityContext, error) {
if response.StatusCode != 200 {
b, _ := io.ReadAll(response.Body)
return nil, fmt.Errorf("response from Cloud Service Provider contains status code %d : %s", response.StatusCode, string(b))
}

defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
fmt.Println(err)
}
}(response.Body)

var jsondata map[string]interface{}
err = json.NewDecoder(res.Body).Decode(&jsondata)
err := json.NewDecoder(response.Body).Decode(&jsondata)
if err != nil {
return nil, fmt.Errorf("error decoding response : %v", err)
}
Expand All @@ -104,7 +175,7 @@ func SecurityContextByRefreshToken(refreshToken string, cspURL string) (core.Sec
return nil, errors.New(errMsg)
}
} else {
return nil, errors.New("Cloud Service Provider authentication response does not contain access token")
return nil, errors.New("cloud Service Provider authentication response does not contain access token")
}

securityCtx := security.NewOauthSecurityContext(accessToken)
Expand Down
7 changes: 6 additions & 1 deletion vmc/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ const (
// DefaultCspURL defines the default URL for CSP.
DefaultCspURL string = "https://console.cloud.vmware.com"

// CspRefreshURLSuffix defines the CSP Refresh API endpoint.
// CspRefreshURLSuffix defines the CSP Refresh Token API endpoint.
CspRefreshURLSuffix string = "/csp/gateway/am/api/auth/api-tokens/authorize"

// CspTokenURLSuffix defines the CSP Oauth API endpoint.
CspTokenURLSuffix string = "/csp/gateway/am/api/auth/token"

// sksNSXTManager to be stripped from nsxt reverse proxy url for public IP resource
SksNSXTManager string = "/sks-nsxt-manager"

Expand Down Expand Up @@ -67,6 +70,8 @@ const (
VmcURL string = "VMC_URL"
CspURL string = "CSP_URL"
APIToken string = "API_TOKEN"
ClientID string = "CLIENT_ID"
ClientSecret string = "CLIENT_SECRET"
OrgID string = "ORG_ID"
OrgDisplayName string = "ORG_DISPLAY_NAME"
// TestSddcID ID of an existing SDDC used for sddc data source, site recovery and srm node tests
Expand Down
9 changes: 7 additions & 2 deletions vmc/data_source_vmc_sddc.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,14 @@ func dataSourceVmcSddcRead(d *schema.ResourceData, m interface{}) error {
d.Set("nsxt_ui", *sddc.ResourceConfig.Nsxt)
if sddc.ResourceConfig.NsxCloudAdmin != nil {
d.Set("nsxt_cloudadmin", *sddc.ResourceConfig.NsxCloudAdmin)
d.Set("nsxt_cloudadmin_password", *sddc.ResourceConfig.NsxCloudAdminPassword)
// Evade nil pointer dereference when user's access_token doesn't have NSX roles
if sddc.ResourceConfig.NsxCloudAdminPassword != nil {
_ = d.Set("nsxt_cloudadmin_password", *sddc.ResourceConfig.NsxCloudAdminPassword)
}
if sddc.ResourceConfig.NsxCloudAuditPassword != nil {
_ = d.Set("nsxt_cloudaudit_password", *sddc.ResourceConfig.NsxCloudAuditPassword)
}
d.Set("nsxt_cloudaudit", *sddc.ResourceConfig.NsxCloudAudit)
d.Set("nsxt_cloudaudit_password", *sddc.ResourceConfig.NsxCloudAuditPassword)
d.Set("nsxt_private_ip", *sddc.ResourceConfig.NsxMgrManagementIp)
d.Set("nsxt_private_url", *sddc.ResourceConfig.NsxMgrLoginUrl)
}
Expand Down
54 changes: 33 additions & 21 deletions vmc/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,34 @@ package vmc

import (
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/vmware/terraform-provider-vmc/vmc/connector"
"github.com/vmware/terraform-provider-vmc/vmc/constants"
"net/http"
"os"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// Provider for VMware VMC Console APIs. Returns terraform.ResourceProvider
func Provider() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"refresh_token": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc(constants.APIToken, nil),
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc(constants.APIToken, nil),
ConflictsWith: []string{"client_id", "client_secret"},
},
"client_id": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc(constants.ClientID, nil),
ConflictsWith: []string{"refresh_token"},
RequiredWith: []string{"client_secret"},
},
"client_secret": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc(constants.ClientSecret, nil),
ConflictsWith: []string{"refresh_token"},
RequiredWith: []string{"client_id"},
},
"org_id": {
Type: schema.TypeString,
Expand Down Expand Up @@ -61,26 +73,26 @@ func Provider() *schema.Provider {

func providerConfigure(d *schema.ResourceData) (interface{}, error) {
refreshToken := d.Get("refresh_token").(string)
if len(refreshToken) <= 0 {
return nil, fmt.Errorf("refresh token cannot be empty")
clientID := d.Get("client_id").(string)
clientSecret := d.Get("client_secret").(string)
if len(refreshToken) == 0 && len(clientID) == 0 && len(clientSecret) == 0 {
return nil, fmt.Errorf("must provide value for refresh_token or client_id and client_secret")
}
// set refresh token to env variable so that it can be used by other connectors
_ = os.Setenv(constants.APIToken, refreshToken)
vmcURL := d.Get("vmc_url").(string)
cspURL := d.Get("csp_url").(string)
_ = os.Setenv(constants.CspURL, cspURL)
orgID := d.Get("org_id").(string)
httpClient := http.Client{}
apiConnector, err := connector.NewClientConnectorByRefreshToken(refreshToken, vmcURL, cspURL, httpClient)
connectorWrapper := connector.Wrapper{
RefreshToken: refreshToken,
ClientID: clientID,
ClientSecret: clientSecret,
OrgID: orgID,
VmcURL: vmcURL,
CspURL: cspURL,
}
err := connectorWrapper.Authenticate()
if err != nil {
return nil, HandleCreateError("Client connector", err)
}

return &connector.Wrapper{
Connector: apiConnector,
RefreshToken: refreshToken,
OrgID: orgID,
VmcURL: vmcURL,
CspURL: cspURL},
nil
return &connectorWrapper, err
}
7 changes: 5 additions & 2 deletions vmc/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ func testAccPreCheckZerocloud(t *testing.T) {
if v := os.Getenv(constants.CspURL); v == "" {
t.Fatal(constants.CspURL + " must be set for Zerocloud acceptance tests")
}
if v := os.Getenv(constants.APIToken); v == "" {
t.Fatal(constants.APIToken + " must be set for acceptance tests")
if v := os.Getenv(constants.ClientID); v == "" {
t.Fatal(constants.ClientID + " must be set for acceptance tests")
}
if v := os.Getenv(constants.ClientSecret); v == "" {
t.Fatal(constants.ClientSecret + " must be set for acceptance tests")
}
if v := os.Getenv(constants.OrgID); v == "" {
t.Fatal(constants.OrgID + " must be set for acceptance tests")
Expand Down
13 changes: 9 additions & 4 deletions vmc/resource_vmc_public_ip.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package vmc

import (
"fmt"
"github.com/vmware/terraform-provider-vmc/vmc/connector"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -59,7 +60,8 @@ func resourcePublicIP() *schema.Resource {

func resourcePublicIPCreate(d *schema.ResourceData, m interface{}) error {
nsxtReverseProxyURL := d.Get("nsxt_reverse_proxy_url").(string)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL)
connectorWrapper := m.(*connector.Wrapper)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL, connectorWrapper)
if err != nil {
return HandleCreateError("NSXT reverse proxy URL connector", err)
}
Expand Down Expand Up @@ -87,7 +89,8 @@ func resourcePublicIPCreate(d *schema.ResourceData, m interface{}) error {

func resourcePublicIPRead(d *schema.ResourceData, m interface{}) error {
nsxtReverseProxyURL := d.Get("nsxt_reverse_proxy_url").(string)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL)
connectorWrapper := m.(*connector.Wrapper)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL, connectorWrapper)
if err != nil {
return HandleCreateError("NSXT reverse proxy URL connector", err)
}
Expand Down Expand Up @@ -124,7 +127,8 @@ func resourcePublicIPRead(d *schema.ResourceData, m interface{}) error {

func resourcePublicIPUpdate(d *schema.ResourceData, m interface{}) error {
nsxtReverseProxyURL := d.Get("nsxt_reverse_proxy_url").(string)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL)
connectorWrapper := m.(*connector.Wrapper)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL, connectorWrapper)
if err != nil {
return HandleCreateError("NSXT reverse proxy URL connector", err)
}
Expand Down Expand Up @@ -154,7 +158,8 @@ func resourcePublicIPUpdate(d *schema.ResourceData, m interface{}) error {

func resourcePublicIPDelete(d *schema.ResourceData, m interface{}) error {
nsxtReverseProxyURL := d.Get("nsxt_reverse_proxy_url").(string)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL)
connectorWrapper := m.(*connector.Wrapper)
connector, err := getNsxtReverseProxyURLConnector(nsxtReverseProxyURL, connectorWrapper)
if err != nil {
return HandleCreateError("NSXT reverse proxy URL connector", err)
}
Expand Down
Loading

0 comments on commit fe9664e

Please sign in to comment.