|
|
|
@ -16,16 +16,15 @@ limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
This file is part of Terraform (by Hashicorp) provider for Digital Energy Cloud Orchestration
|
|
|
|
|
This file is part of Terraform (by Hashicorp) provider for Digital Energy Cloud Orchestration
|
|
|
|
|
Technology platfom.
|
|
|
|
|
|
|
|
|
|
Visit https://github.com/rudecs/terraform-provider-decort for full source code package and updates.
|
|
|
|
|
Visit https://github.com/rudecs/terraform-provider-decort for full source code package and updates.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package decort
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
|
|
|
|
"bytes"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"fmt"
|
|
|
|
@ -34,6 +33,7 @@ import (
|
|
|
|
|
"net/url"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
// "time"
|
|
|
|
|
|
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
@ -42,42 +42,41 @@ import (
|
|
|
|
|
|
|
|
|
|
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
|
|
|
|
|
// "github.com/hashicorp/terraform-plugin-sdk/terraform"
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// enumerated constants that define authentication modes
|
|
|
|
|
// enumerated constants that define authentication modes
|
|
|
|
|
const (
|
|
|
|
|
MODE_UNDEF = iota // this is the invalid mode - it should never be seen
|
|
|
|
|
MODE_LEGACY = iota
|
|
|
|
|
MODE_OAUTH2 = iota
|
|
|
|
|
MODE_JWT = iota
|
|
|
|
|
MODE_UNDEF = iota // this is the invalid mode - it should never be seen
|
|
|
|
|
MODE_LEGACY = iota
|
|
|
|
|
MODE_OAUTH2 = iota
|
|
|
|
|
MODE_JWT = iota
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ControllerCfg struct {
|
|
|
|
|
controller_url string // always required
|
|
|
|
|
auth_mode_code int // always required
|
|
|
|
|
auth_mode_txt string // always required, it is a text representation of auth mode
|
|
|
|
|
legacy_user string // required for legacy mode
|
|
|
|
|
legacy_password string // required for legacy mode
|
|
|
|
|
legacy_sid string // obtained from DECORT controller on successful login in legacy mode
|
|
|
|
|
jwt string // obtained from Outh2 provider on successful login in oauth2 mode, required in jwt mode
|
|
|
|
|
app_id string // required for oauth2 mode
|
|
|
|
|
app_secret string // required for oauth2 mode
|
|
|
|
|
oauth2_url string // always required
|
|
|
|
|
decort_username string // assigned to either legacy_user (legacy mode) or Oauth2 user (oauth2 mode) upon successful verification
|
|
|
|
|
cc_client *http.Client // assigned when all initial checks successfully passed
|
|
|
|
|
controller_url string // always required
|
|
|
|
|
auth_mode_code int // always required
|
|
|
|
|
auth_mode_txt string // always required, it is a text representation of auth mode
|
|
|
|
|
legacy_user string // required for legacy mode
|
|
|
|
|
legacy_password string // required for legacy mode
|
|
|
|
|
legacy_sid string // obtained from DECORT controller on successful login in legacy mode
|
|
|
|
|
jwt string // obtained from Outh2 provider on successful login in oauth2 mode, required in jwt mode
|
|
|
|
|
app_id string // required for oauth2 mode
|
|
|
|
|
app_secret string // required for oauth2 mode
|
|
|
|
|
oauth2_url string // always required
|
|
|
|
|
decort_username string // assigned to either legacy_user (legacy mode) or Oauth2 user (oauth2 mode) upon successful verification
|
|
|
|
|
cc_client *http.Client // assigned when all initial checks successfully passed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
// This function first will check that all required provider parameters for the
|
|
|
|
|
// This function first will check that all required provider parameters for the
|
|
|
|
|
// selected authenticator mode are set correctly and initialize ControllerCfg structure
|
|
|
|
|
// based on the provided parameters.
|
|
|
|
|
//
|
|
|
|
|
// Next, it will check for validity of supplied credentials by initiating connection to the specified
|
|
|
|
|
// DECORT controller URL and, if succeeded, completes ControllerCfg structure with the rest of computed
|
|
|
|
|
// Next, it will check for validity of supplied credentials by initiating connection to the specified
|
|
|
|
|
// DECORT controller URL and, if succeeded, completes ControllerCfg structure with the rest of computed
|
|
|
|
|
// parameters (e.g. JWT, session ID and Oauth2 user name).
|
|
|
|
|
//
|
|
|
|
|
// The structure created by this function should be used with subsequent calls to decortAPICall() method,
|
|
|
|
|
// The structure created by this function should be used with subsequent calls to decortAPICall() method,
|
|
|
|
|
// which is a DECORT authentication mode aware wrapper around standard HTTP requests.
|
|
|
|
|
|
|
|
|
|
ret_config := &ControllerCfg{
|
|
|
|
@ -90,7 +89,7 @@ func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
app_id: d.Get("app_id").(string),
|
|
|
|
|
app_secret: d.Get("app_secret").(string),
|
|
|
|
|
oauth2_url: d.Get("oauth2_url").(string),
|
|
|
|
|
decort_username: "",
|
|
|
|
|
decort_username: "",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var allow_unverified_ssl bool
|
|
|
|
@ -137,10 +136,10 @@ func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
|
|
|
|
|
if allow_unverified_ssl {
|
|
|
|
|
log.Warn("ControllerConfigure: allow_unverified_ssl is set - will not check certificates!")
|
|
|
|
|
transCfg := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true},}
|
|
|
|
|
transCfg := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
|
|
|
|
ret_config.cc_client = &http.Client{
|
|
|
|
|
Transport: transCfg,
|
|
|
|
|
Timeout: Timeout180s,
|
|
|
|
|
Timeout: Timeout180s,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ret_config.cc_client = &http.Client{
|
|
|
|
@ -150,7 +149,7 @@ func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
|
|
|
|
|
switch ret_config.auth_mode_code {
|
|
|
|
|
case MODE_LEGACY:
|
|
|
|
|
ok, err := ret_config.validateLegacyUser()
|
|
|
|
|
ok, err := ret_config.validateLegacyUser()
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
@ -162,13 +161,13 @@ func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
case MODE_OAUTH2:
|
|
|
|
|
// on success getOAuth2JWT will set config.jwt to the obtained JWT, so there is no
|
|
|
|
|
// on success getOAuth2JWT will set config.jwt to the obtained JWT, so there is no
|
|
|
|
|
// need to set it once again here
|
|
|
|
|
_, err := ret_config.getOAuth2JWT()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
// we are not verifying the JWT when parsing because actual verification is done on the
|
|
|
|
|
// we are not verifying the JWT when parsing because actual verification is done on the
|
|
|
|
|
// OVC controller side. Here we do parsing solely to extract Oauth2 user name (claim "user")
|
|
|
|
|
// and JWT issuer name (claim "iss")
|
|
|
|
|
parser := jwt.Parser{}
|
|
|
|
@ -181,7 +180,7 @@ func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
tbuf.WriteString(claims["username"].(string))
|
|
|
|
|
tbuf.WriteString("@")
|
|
|
|
|
tbuf.WriteString(claims["iss"].(string))
|
|
|
|
|
ret_config.decort_username = tbuf.String()
|
|
|
|
|
ret_config.decort_username = tbuf.String()
|
|
|
|
|
} else {
|
|
|
|
|
return nil, fmt.Errorf("Failed to extract user and iss fields from JWT token in oauth2 mode.")
|
|
|
|
|
}
|
|
|
|
@ -195,7 +194,7 @@ func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
|
|
|
|
|
return ret_config, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (config *ControllerCfg) getDecortUsername() (string) {
|
|
|
|
|
func (config *ControllerCfg) getDecortUsername() string {
|
|
|
|
|
return config.decort_username
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -216,14 +215,14 @@ func (config *ControllerCfg) getOAuth2JWT() (string, error) {
|
|
|
|
|
params.Add("validity", "3600")
|
|
|
|
|
params_str := params.Encode()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", config.oauth2_url + "/v1/oauth/access_token", strings.NewReader(params_str))
|
|
|
|
|
req, err := http.NewRequest("POST", config.oauth2_url+"/v1/oauth/access_token", strings.NewReader(params_str))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))
|
|
|
|
|
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
@ -231,16 +230,16 @@ func (config *ControllerCfg) getOAuth2JWT() (string, error) {
|
|
|
|
|
// fmt.Println("response Status:", resp.Status)
|
|
|
|
|
// fmt.Println("response Headers:", resp.Header)
|
|
|
|
|
// fmt.Println("response Headers:", req.URL)
|
|
|
|
|
return "", fmt.Errorf("getOauth2JWT: unexpected status code %d when obtaining JWT from %q for APP_ID %q, request Body %q",
|
|
|
|
|
resp.StatusCode, req.URL, config.app_id, params_str)
|
|
|
|
|
return "", fmt.Errorf("getOauth2JWT: unexpected status code %d when obtaining JWT from %q for APP_ID %q, request Body %q",
|
|
|
|
|
resp.StatusCode, req.URL, config.app_id, params_str)
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
responseData, err := ioutil.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validation successful - store JWT in the corresponding field of the ControllerCfg structure
|
|
|
|
|
config.jwt = strings.TrimSpace(string(responseData))
|
|
|
|
|
|
|
|
|
@ -249,10 +248,10 @@ func (config *ControllerCfg) getOAuth2JWT() (string, error) {
|
|
|
|
|
|
|
|
|
|
func (config *ControllerCfg) validateJWT(jwt string) (bool, error) {
|
|
|
|
|
/*
|
|
|
|
|
Validate JWT against DECORT controller. JWT can be supplied as argument to this method. If empty string supplied as
|
|
|
|
|
argument, JWT will be taken from config attribute.
|
|
|
|
|
DECORT controller URL will always be taken from the config attribute assigned at instantiation.
|
|
|
|
|
Validation is accomplished by attempting API call that lists accounts for the invoking user.
|
|
|
|
|
Validate JWT against DECORT controller. JWT can be supplied as argument to this method. If empty string supplied as
|
|
|
|
|
argument, JWT will be taken from config attribute.
|
|
|
|
|
DECORT controller URL will always be taken from the config attribute assigned at instantiation.
|
|
|
|
|
Validation is accomplished by attempting API call that lists accounts for the invoking user.
|
|
|
|
|
*/
|
|
|
|
|
if jwt == "" {
|
|
|
|
|
if config.jwt == "" {
|
|
|
|
@ -265,7 +264,7 @@ func (config *ControllerCfg) validateJWT(jwt string) (bool, error) {
|
|
|
|
|
return false, fmt.Errorf("validateJWT method called, but no OAuth2 URL provided.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", config.controller_url + "/restmachine/cloudapi/accounts/list", nil)
|
|
|
|
|
req, err := http.NewRequest("POST", config.controller_url+"/restmachine/cloudapi/accounts/list", nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
@ -273,14 +272,14 @@ func (config *ControllerCfg) validateJWT(jwt string) (bool, error) {
|
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", jwt))
|
|
|
|
|
// req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
// req.Header.Set("Content-Length", strconv.Itoa(0))
|
|
|
|
|
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return false, fmt.Errorf("validateJWT: unexpected status code %d when validating JWT against %q.",
|
|
|
|
|
resp.StatusCode, req.URL)
|
|
|
|
|
return false, fmt.Errorf("validateJWT: unexpected status code %d when validating JWT against %q.",
|
|
|
|
|
resp.StatusCode, req.URL)
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
@ -289,10 +288,10 @@ func (config *ControllerCfg) validateJWT(jwt string) (bool, error) {
|
|
|
|
|
|
|
|
|
|
func (config *ControllerCfg) validateLegacyUser() (bool, error) {
|
|
|
|
|
/*
|
|
|
|
|
Validate legacy user by obtaining a session key, which will be used for authenticating subsequent API calls
|
|
|
|
|
to DECORT controller.
|
|
|
|
|
If successful, the session key is stored in config.legacy_sid and true is returned. If unsuccessful for any
|
|
|
|
|
reason, the method will return false and error.
|
|
|
|
|
Validate legacy user by obtaining a session key, which will be used for authenticating subsequent API calls
|
|
|
|
|
to DECORT controller.
|
|
|
|
|
If successful, the session key is stored in config.legacy_sid and true is returned. If unsuccessful for any
|
|
|
|
|
reason, the method will return false and error.
|
|
|
|
|
*/
|
|
|
|
|
if config.auth_mode_code == MODE_UNDEF {
|
|
|
|
|
return false, fmt.Errorf("validateLegacyUser method called for undefined authorization mode.")
|
|
|
|
@ -306,7 +305,7 @@ func (config *ControllerCfg) validateLegacyUser() (bool, error) {
|
|
|
|
|
params.Add("password", config.legacy_password)
|
|
|
|
|
params_str := params.Encode()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", config.controller_url + "/restmachine/cloudapi/users/authenticate", strings.NewReader(params_str))
|
|
|
|
|
req, err := http.NewRequest("POST", config.controller_url+"/restmachine/cloudapi/users/authenticate", strings.NewReader(params_str))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
@ -314,21 +313,21 @@ func (config *ControllerCfg) validateLegacyUser() (bool, error) {
|
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))
|
|
|
|
|
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return false, fmt.Errorf("validateLegacyUser: unexpected status code %d when validating legacy user %q against %q.",
|
|
|
|
|
resp.StatusCode, config.legacy_user, config.controller_url)
|
|
|
|
|
return false, fmt.Errorf("validateLegacyUser: unexpected status code %d when validating legacy user %q against %q.",
|
|
|
|
|
resp.StatusCode, config.legacy_user, config.controller_url)
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
responseData, err := ioutil.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validation successful - keep session ID for future use
|
|
|
|
|
config.legacy_sid = string(responseData)
|
|
|
|
|
|
|
|
|
@ -336,16 +335,16 @@ func (config *ControllerCfg) validateLegacyUser() (bool, error) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (config *ControllerCfg) decortAPICall(method string, api_name string, url_values *url.Values) (json_resp string, err error) {
|
|
|
|
|
// This is a convenience wrapper around standard HTTP request methods that is aware of the
|
|
|
|
|
// This is a convenience wrapper around standard HTTP request methods that is aware of the
|
|
|
|
|
// authorization mode for which the provider was initialized and compiles request accordingly.
|
|
|
|
|
|
|
|
|
|
if config.cc_client == nil {
|
|
|
|
|
// this should never happen if ClientConfig was properly called prior to decortAPICall
|
|
|
|
|
// this should never happen if ClientConfig was properly called prior to decortAPICall
|
|
|
|
|
return "", fmt.Errorf("decortAPICall method called with unconfigured DECORT cloud controller HTTP client.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Example: to create api_params, one would generally do the following:
|
|
|
|
|
//
|
|
|
|
|
//
|
|
|
|
|
// data := []byte(`{"machineId": "2638"}`)
|
|
|
|
|
// api_params := bytes.NewBuffer(data))
|
|
|
|
|
//
|
|
|
|
@ -367,42 +366,41 @@ func (config *ControllerCfg) decortAPICall(method string, api_name string, url_v
|
|
|
|
|
}
|
|
|
|
|
params_str := url_values.Encode()
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(method, config.controller_url + api_name, strings.NewReader(params_str))
|
|
|
|
|
req, err := http.NewRequest(method, config.controller_url+api_name, strings.NewReader(params_str))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(params_str)))
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
|
|
|
|
|
if config.auth_mode_code == MODE_OAUTH2 || config.auth_mode_code == MODE_JWT {
|
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", config.jwt))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := config.cc_client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
|
tmp_body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
json_resp := Jo2JSON(string(tmp_body))
|
|
|
|
|
log.Debugf("decortAPICall: %s %s\n %s", method, api_name, json_resp)
|
|
|
|
|
return json_resp, nil
|
|
|
|
|
}
|
|
|
|
|
log.Debugf("decortAPICall: %s %s\n %s", method, api_name, body)
|
|
|
|
|
return string(body), nil
|
|
|
|
|
} else {
|
|
|
|
|
return "", fmt.Errorf("decortAPICall: unexpected status code %d when calling API %q with request Body %q",
|
|
|
|
|
resp.StatusCode, req.URL, params_str)
|
|
|
|
|
return "", fmt.Errorf("decortAPICall: unexpected status code %d when calling API %q with request Body %q",
|
|
|
|
|
resp.StatusCode, req.URL, params_str)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
if resp.StatusCode == StatusServiceUnavailable {
|
|
|
|
|
return nil, fmt.Errorf("decortAPICall method called for incompatible authorization mode %q.", config.auth_mode_txt)
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode == StatusServiceUnavailable {
|
|
|
|
|
return nil, fmt.Errorf("decortAPICall method called for incompatible authorization mode %q.", config.auth_mode_txt)
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|