You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
terraform-provider-decort/internal/controller/controller.go

433 lines
16 KiB

/*
Copyright (c) 2019-2023 Digital Energy Cloud Solutions LLC. All Rights Reserved.
Authors:
Petr Krutov, <petr.krutov@digitalenergy.online>
Stanislav Solovev, <spsolovev@digitalenergy.online>
Kasim Baybikov, <kmbaybikov@basistech.ru>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
decort "repository.basistech.ru/BASIS/decort-golang-sdk"
"repository.basistech.ru/BASIS/decort-golang-sdk/config"
"repository.basistech.ru/BASIS/decort-golang-sdk/interfaces"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi"
"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
// enumerated constants that define authentication modes
const (
MODE_UNDEF = iota // this is the invalid mode - it should never be seen
MODE_LEGACY
MODE_DECS3O
MODE_JWT
MODE_BVS
)
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
bvs_user string // required for bvs mode
bvs_password string // required for bvs mode
domain string // required for bvs mode
token config.Token // obtained from BVS provider on successful login in bvs mode
path_cfg string // the path of the configuration file entry
path_token string // the path of the token file entry
time_to_refresh int64 // the number of minutes before the expiration of the token, a refresh will be made
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 decs3o mode, required in jwt mode
app_id string // required for decs3o and bvs mode
app_secret string // required for decs3o and bvs mode
oauth2_url string // required for decs3o and bvs mode
decort_username string // assigned to either legacy_user (legacy mode) or Oauth2 user (decs3o mode) upon successful verification
cc_client *http.Client // assigned when all initial checks successfully passed
caller interfaces.Caller
}
func ControllerConfigure(d *schema.ResourceData) (*ControllerCfg, error) {
// 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
// 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,
// which is a DECORT authentication mode aware wrapper around standard HTTP requests.
ret_config := &ControllerCfg{
controller_url: d.Get("controller_url").(string),
auth_mode_code: MODE_UNDEF,
legacy_user: d.Get("user").(string),
legacy_password: d.Get("password").(string),
legacy_sid: "",
bvs_user: d.Get("bvs_user").(string),
bvs_password: d.Get("bvs_password").(string),
domain: d.Get("domain").(string),
jwt: d.Get("jwt").(string),
app_id: d.Get("app_id").(string),
app_secret: d.Get("app_secret").(string),
oauth2_url: d.Get("oauth2_url").(string),
decort_username: "",
token: config.Token{},
path_cfg: d.Get("path_cfg").(string),
path_token: d.Get("path_token").(string),
time_to_refresh: int64(d.Get("time_to_refresh").(int)),
}
allow_unverified_ssl := d.Get("allow_unverified_ssl").(bool)
if ret_config.controller_url == "" {
return nil, fmt.Errorf("empty DECORT cloud controller URL provided")
}
// this should have already been done by StateFunc defined in Schema, but we want to be sure
ret_config.auth_mode_txt = strings.ToLower(d.Get("authenticator").(string))
switch ret_config.auth_mode_txt {
case "jwt":
if ret_config.jwt == "" {
return nil, fmt.Errorf("authenticator mode 'jwt' specified but no JWT provided")
}
ret_config.auth_mode_code = MODE_JWT
case "decs3o":
if ret_config.oauth2_url == "" {
return nil, fmt.Errorf("authenticator mode 'decs3o' specified but no OAuth2 URL provided")
}
if ret_config.app_id == "" {
return nil, fmt.Errorf("authenticator mode 'decs3o' specified but no Application ID provided")
}
if ret_config.app_secret == "" {
return nil, fmt.Errorf("authenticator mode 'decs3o' specified but no Secret ID provided")
}
ret_config.auth_mode_code = MODE_DECS3O
case "legacy":
//
ret_config.legacy_user = d.Get("user").(string)
if ret_config.legacy_user == "" {
return nil, fmt.Errorf("authenticator mode 'legacy' specified but no user provided")
}
ret_config.legacy_password = d.Get("password").(string)
if ret_config.legacy_password == "" {
return nil, fmt.Errorf("authenticator mode 'legacy' specified but no password provided")
}
ret_config.auth_mode_code = MODE_LEGACY
case "bvs":
if ret_config.bvs_user == "" {
return nil, fmt.Errorf("authenticator mode 'bvs' specified but no user provided")
}
if ret_config.bvs_password == "" {
return nil, fmt.Errorf("authenticator mode 'bvs' specified but no password provided")
}
if ret_config.oauth2_url == "" {
return nil, fmt.Errorf("authenticator mode 'bvs' specified but no bvs URL provided")
}
if ret_config.app_id == "" {
return nil, fmt.Errorf("authenticator mode 'bvs' specified but no Application ID provided")
}
if ret_config.app_secret == "" {
return nil, fmt.Errorf("authenticator mode 'bvs' specified but no Secret ID provided")
}
if ret_config.domain == "" {
return nil, fmt.Errorf("authenticator mode 'bvs' specified but no Domain provided")
}
ret_config.auth_mode_code = MODE_BVS
default:
return nil, fmt.Errorf("unknown authenticator mode %q provided", ret_config.auth_mode_txt)
}
if allow_unverified_ssl {
log.Warn("ControllerConfigure: allow_unverified_ssl is set - will not check certificates!")
transCfg := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec
ret_config.cc_client = &http.Client{
Transport: transCfg,
}
} else {
ret_config.cc_client = &http.Client{}
}
switch ret_config.auth_mode_code {
case MODE_LEGACY:
ok, err := ret_config.validateLegacyUser()
if !ok {
return nil, err
}
ret_config.decort_username = ret_config.legacy_user
sdkConf := config.LegacyConfig{
Username: ret_config.legacy_user,
Password: ret_config.legacy_password,
DecortURL: ret_config.controller_url,
SSLSkipVerify: allow_unverified_ssl,
}
ret_config.caller = decort.NewLegacy(sdkConf)
case MODE_JWT:
//
ok, err := ret_config.validateJWT("")
if !ok {
return nil, err
}
case MODE_DECS3O:
// on success getDECS3OJWT will set config.jwt to the obtained JWT, so there is no
// need to set it once again here
// _, err := ret_config.getDECS3OJWT()
// if err != nil {
// return nil, err
// }
// 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{}
// token, _, err := parser.ParseUnverified(ret_config.jwt, jwt.MapClaims{})
// if err != nil {
// return nil, err
// }
// if claims, ok := token.Claims.(jwt.MapClaims); ok {
// var tbuf bytes.Buffer
// tbuf.WriteString(claims["username"].(string))
// tbuf.WriteString("@")
// tbuf.WriteString(claims["iss"].(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")
// }
sdkConf := config.Config{
AppID: ret_config.app_id,
AppSecret: ret_config.app_secret,
SSOURL: ret_config.oauth2_url,
DecortURL: ret_config.controller_url,
SSLSkipVerify: allow_unverified_ssl,
}
ret_config.caller = decort.New(sdkConf)
case MODE_BVS:
sdkConf := config.BVSConfig{
AppID: ret_config.app_id,
AppSecret: ret_config.app_secret,
SSOURL: ret_config.oauth2_url,
DecortURL: ret_config.controller_url,
SSLSkipVerify: allow_unverified_ssl,
Username: ret_config.bvs_user,
Password: ret_config.bvs_password,
Domain: ret_config.domain,
Token: ret_config.token,
PathCfg: ret_config.path_cfg,
PathToken: ret_config.path_token,
TimeToRefresh: ret_config.time_to_refresh,
}
ret_config.caller = decort.NewBVS(sdkConf)
default:
// FYI, this should never happen due to all above checks, but we want to be fool proof
return nil, fmt.Errorf("unknown authenticator mode code %d provided", ret_config.auth_mode_code)
}
// All checks passed successfully, credentials corresponding to the selected authenticator mode
// obtained and validated.
return ret_config, nil
}
// func (config *ControllerCfg) GetDecortUsername() string {
// return config.decort_username
// }
// func (config *ControllerCfg) getDECS3OJWT() (string, error) {
// // Obtain JWT from the Oauth2 provider using application ID and application secret provided in config.
// if config.auth_mode_code == MODE_UNDEF {
// return "", fmt.Errorf("getOAuth2JWT method called for undefined authorization mode")
// }
// if config.auth_mode_code != MODE_DECS3O {
// return "", fmt.Errorf("getOAuth2JWT method called for incompatible authorization mode %q", config.auth_mode_txt)
// }
// params := url.Values{}
// params.Add("grant_type", "client_credentials")
// params.Add("client_id", config.app_id)
// params.Add("client_secret", config.app_secret)
// params.Add("response_type", "id_token")
// params.Add("validity", "3600")
// params_str := params.Encode()
// 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)
// if err != nil {
// return "", err
// }
// if resp.StatusCode != http.StatusOK {
// // 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)
// }
// defer resp.Body.Close()
// responseData, err := io.ReadAll(resp.Body)
// if err != nil {
// return "", err
// }
// // validation successful - store JWT in the corresponding field of the ControllerCfg structure
// config.jwt = strings.TrimSpace(string(responseData))
// return config.jwt, nil
// }
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 account for the invoking user.
*/
if jwt == "" {
if config.jwt == "" {
return false, fmt.Errorf("validateJWT method called, but no meaningful JWT provided")
}
jwt = config.jwt
}
if config.oauth2_url == "" {
return false, fmt.Errorf("validateJWT method called, but no OAuth2 URL provided")
}
req, err := http.NewRequest("POST", config.controller_url+"/restmachine/cloudapi/account/list", nil)
if err != nil {
return false, err
}
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)
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)
}
defer resp.Body.Close()
return true, nil
}
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.
*/
if config.auth_mode_code == MODE_UNDEF {
return false, fmt.Errorf("validateLegacyUser method called for undefined authorization mode")
}
if config.auth_mode_code != MODE_LEGACY {
return false, fmt.Errorf("validateLegacyUser method called for incompatible authorization mode %q", config.auth_mode_txt)
}
params := url.Values{}
params.Add("username", config.legacy_user)
params.Add("password", config.legacy_password)
params_str := params.Encode()
req, err := http.NewRequest("POST", config.controller_url+"/restmachine/cloudapi/user/authenticate", strings.NewReader(params_str))
if err != nil {
return false, 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)
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)
}
defer resp.Body.Close()
responseData, err := io.ReadAll(req.Body)
if err != nil {
log.Fatal(err)
}
// validation successful - keep session ID for future use
config.legacy_sid = string(responseData)
return true, nil
}
func (config *ControllerCfg) CloudAPI() *cloudapi.CloudAPI {
switch config.auth_mode_code {
case MODE_LEGACY:
client, _ := config.caller.(*decort.LegacyDecortClient)
return client.CloudAPI()
case MODE_DECS3O:
client, _ := config.caller.(*decort.DecortClient)
return client.CloudAPI()
case MODE_BVS:
client, _ := config.caller.(*decort.BVSDecortClient)
return client.CloudAPI()
default:
return &cloudapi.CloudAPI{}
}
}
func (config *ControllerCfg) CloudBroker() *cloudbroker.CloudBroker {
switch config.auth_mode_code {
case MODE_LEGACY:
client, _ := config.caller.(*decort.LegacyDecortClient)
return client.CloudBroker()
case MODE_DECS3O:
client, _ := config.caller.(*decort.DecortClient)
return client.CloudBroker()
case MODE_BVS:
client, _ := config.caller.(*decort.BVSDecortClient)
return client.CloudBroker()
default:
return &cloudbroker.CloudBroker{}
}
}