mirror of https://github.com/hashicorp/terraform
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.
471 lines
17 KiB
471 lines
17 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package oci
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/version"
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
"github.com/oracle/oci-go-sdk/v65/common/auth"
|
|
"github.com/oracle/oci-go-sdk/v65/objectstorage"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
var ApiKeyConfigAttributes = [5]string{UserOcidAttrName, FingerprintAttrName, PrivateKeyAttrName, PrivateKeyPathAttrName, PrivateKeyPasswordAttrName}
|
|
|
|
type ociAuthConfigProvider struct {
|
|
authType string
|
|
configFileProfile string
|
|
region string
|
|
tenancyOcid string
|
|
userOcid string
|
|
fingerprint string
|
|
privateKey string
|
|
privateKeyPath string
|
|
privateKeyPassword string
|
|
}
|
|
|
|
func newOciAuthConfigProvider(obj cty.Value) ociAuthConfigProvider {
|
|
p := ociAuthConfigProvider{}
|
|
|
|
if authVal, ok := getBackendAttrWithDefault(obj, AuthAttrName, AuthAPIKeySetting); ok {
|
|
p.authType = authVal.AsString()
|
|
}
|
|
|
|
if configFileProfileVal, ok := getBackendAttr(obj, ConfigFileProfileAttrName); ok {
|
|
p.configFileProfile = configFileProfileVal.AsString()
|
|
}
|
|
if regionVal, ok := getBackendAttr(obj, RegionAttrName); ok {
|
|
p.region = regionVal.AsString()
|
|
}
|
|
|
|
if tenancyOcidVal, ok := getBackendAttr(obj, TenancyOcidAttrName); ok {
|
|
p.tenancyOcid = tenancyOcidVal.AsString()
|
|
}
|
|
|
|
if userOcidVal, ok := getBackendAttr(obj, UserOcidAttrName); ok {
|
|
p.userOcid = userOcidVal.AsString()
|
|
}
|
|
|
|
if fingerprintVal, ok := getBackendAttr(obj, FingerprintAttrName); ok {
|
|
p.fingerprint = fingerprintVal.AsString()
|
|
}
|
|
|
|
if privateKeyVal, ok := getBackendAttr(obj, PrivateKeyAttrName); ok {
|
|
p.privateKey = privateKeyVal.AsString()
|
|
}
|
|
|
|
if privateKeyPathVal, ok := getBackendAttr(obj, PrivateKeyPathAttrName); ok {
|
|
p.privateKeyPath = privateKeyPathVal.AsString()
|
|
}
|
|
|
|
if privateKeyPasswordVal, ok := getBackendAttr(obj, PrivateKeyPasswordAttrName); ok {
|
|
p.privateKeyPassword = privateKeyPasswordVal.AsString()
|
|
}
|
|
|
|
return p
|
|
}
|
|
func (p ociAuthConfigProvider) AuthType() (common.AuthConfig, error) {
|
|
return common.AuthConfig{
|
|
AuthType: common.UnknownAuthenticationType,
|
|
IsFromConfigFile: false,
|
|
OboToken: nil,
|
|
},
|
|
fmt.Errorf("unsupported, keep the interface")
|
|
}
|
|
|
|
func (p ociAuthConfigProvider) TenancyOCID() (string, error) {
|
|
if p.tenancyOcid != "" {
|
|
return p.tenancyOcid, nil
|
|
}
|
|
return "", fmt.Errorf("can not get %s from Terraform backend configuration", TenancyOcidAttrName)
|
|
}
|
|
|
|
func (p ociAuthConfigProvider) UserOCID() (string, error) {
|
|
if p.userOcid != "" {
|
|
return p.userOcid, nil
|
|
}
|
|
return "", fmt.Errorf("can not get %s from Terraform backend configuration", UserOcidAttrName)
|
|
}
|
|
|
|
func (p ociAuthConfigProvider) KeyFingerprint() (string, error) {
|
|
if p.fingerprint != "" {
|
|
return p.fingerprint, nil
|
|
}
|
|
return "", fmt.Errorf("can not get %s from Terraform backend configuration", FingerprintAttrName)
|
|
}
|
|
|
|
func (p ociAuthConfigProvider) Region() (string, error) {
|
|
if p.region != "" {
|
|
return p.region, nil
|
|
}
|
|
return "", fmt.Errorf("can not get %s from Terraform backend configuration", RegionAttrName)
|
|
}
|
|
func (p ociAuthConfigProvider) KeyID() (string, error) {
|
|
tenancy, err := p.TenancyOCID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
user, err := p.UserOCID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
fingerprint, err := p.KeyFingerprint()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil
|
|
}
|
|
|
|
func (p ociAuthConfigProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {
|
|
|
|
if p.privateKey != "" {
|
|
keyData := strings.ReplaceAll(p.privateKey, "\\n", "\n") // Ensure \n is replaced by actual newlines
|
|
return common.PrivateKeyFromBytesWithPassword([]byte(keyData), []byte(p.privateKeyPassword))
|
|
}
|
|
|
|
if p.privateKeyPath != "" {
|
|
resolvedPath := expandPath(p.privateKeyPath)
|
|
pemFileContent, readFileErr := os.ReadFile(resolvedPath)
|
|
if readFileErr != nil {
|
|
return nil, fmt.Errorf("can not read private key from: '%s', Error: %q", p.privateKeyPath, readFileErr)
|
|
}
|
|
return common.PrivateKeyFromBytesWithPassword(pemFileContent, []byte(p.privateKeyPassword))
|
|
}
|
|
|
|
return nil, fmt.Errorf("can not get private_key or private_key_path from Terraform configuration")
|
|
}
|
|
|
|
func (p ociAuthConfigProvider) getConfigProviders() ([]common.ConfigurationProvider, error) {
|
|
var configProviders []common.ConfigurationProvider
|
|
logger := logWithOperation("AuthConfigProvider")
|
|
logger.Debug(fmt.Sprintf("Using %s authentication", p.authType))
|
|
switch strings.ToLower(p.authType) {
|
|
case strings.ToLower(AuthAPIKeySetting):
|
|
// No additional config providers needed
|
|
case strings.ToLower(AuthInstancePrincipalSetting):
|
|
|
|
logger.Info("Attempting to authenticate using instance principal credentials")
|
|
if p.region == "" {
|
|
return nil, fmt.Errorf("unable to determine region from Terraform backend configuration while using Instance Principal")
|
|
}
|
|
|
|
// Used to modify InstancePrincipal auth clients so that `accept_local_certs` is honored for auth clients as well
|
|
instancePrincipalAuthClientModifier := func(client common.HTTPRequestDispatcher) (common.HTTPRequestDispatcher, error) {
|
|
if acceptLocalCerts := getEnvSettingWithBlankDefault(AcceptLocalCerts); acceptLocalCerts != "" {
|
|
if value, err := strconv.ParseBool(acceptLocalCerts); err == nil {
|
|
modifiedClient := buildHttpClient()
|
|
modifiedClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = value
|
|
return modifiedClient, nil
|
|
}
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
cfg, err := auth.InstancePrincipalConfigurationForRegionWithCustomClient(common.StringToRegion(p.region), instancePrincipalAuthClientModifier)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logger.Debug(" Configuration provided by: %s", cfg)
|
|
|
|
configProviders = append(configProviders, cfg)
|
|
case strings.ToLower(AuthInstancePrincipalWithCertsSetting):
|
|
logger.Info("Attempting to authenticate using instance principal with certificates")
|
|
|
|
if p.region == "" {
|
|
return nil, fmt.Errorf("unable to determine region from Terraform backend configuration while using Instance Principal with certificates")
|
|
}
|
|
|
|
defaultCertsDir, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can not get working directory for current os platform")
|
|
}
|
|
|
|
certsDir := filepath.Clean(getEnvSettingWithDefault("test_certificates_location", defaultCertsDir))
|
|
leafCertificateBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "ip_cert.pem"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can not read leaf certificate from %s", filepath.Join(certsDir, "ip_cert.pem"))
|
|
}
|
|
|
|
leafPrivateKeyBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "ip_key.pem"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can not read leaf private key from %s", filepath.Join(certsDir, "ip_key.pem"))
|
|
}
|
|
|
|
leafPassphraseBytes := []byte{}
|
|
if _, err := os.Stat(certsDir + "/leaf_passphrase"); !os.IsNotExist(err) {
|
|
leafPassphraseBytes, err = getCertificateFileBytes(filepath.Join(certsDir + "leaf_passphrase"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can not read leafPassphraseBytes from %s", filepath.Join(certsDir+"leaf_passphrase"))
|
|
}
|
|
}
|
|
|
|
intermediateCertificateBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "intermediate.pem"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can not read intermediate certificate from %s", filepath.Join(certsDir, "intermediate.pem"))
|
|
}
|
|
|
|
intermediateCertificatesBytes := [][]byte{
|
|
intermediateCertificateBytes,
|
|
}
|
|
|
|
cfg, err := auth.InstancePrincipalConfigurationWithCerts(common.StringToRegion(p.region), leafCertificateBytes, leafPassphraseBytes, leafPrivateKeyBytes, intermediateCertificatesBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logger.Debug(" Configuration provided by: %s", cfg)
|
|
|
|
configProviders = append(configProviders, cfg)
|
|
|
|
case strings.ToLower(AuthSecurityToken):
|
|
logger.Info("Attempting to authenticate using security token")
|
|
if p.region == "" {
|
|
return nil, fmt.Errorf("can not get %s from Terraform configuration (SecurityToken)", RegionAttrName)
|
|
}
|
|
// if region is part of the provider block make sure it is part of the final configuration too, and overwrites the region in the profile. +
|
|
regionProvider := common.NewRawConfigurationProvider("", "", p.region, "", "", nil)
|
|
configProviders = append(configProviders, regionProvider)
|
|
|
|
if p.configFileProfile == "" {
|
|
return nil, fmt.Errorf("missing profile in provider block %v", ConfigFileProfileAttrName)
|
|
}
|
|
|
|
defaultPath := path.Join(getHomeFolder(), DefaultConfigDirName, DefaultConfigFileName)
|
|
if err := checkProfile(p.configFileProfile, defaultPath); err != nil {
|
|
return nil, err
|
|
}
|
|
securityTokenBasedAuthConfigProvider, err := common.ConfigurationProviderForSessionTokenWithProfile(defaultPath, p.configFileProfile, p.privateKeyPassword)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create security token based auth config provider %v", err)
|
|
}
|
|
configProviders = append(configProviders, securityTokenBasedAuthConfigProvider)
|
|
case strings.ToLower(ResourcePrincipal):
|
|
logger.Info("Attempting to authenticate using resource principal credentials")
|
|
var err error
|
|
var resourcePrincipalAuthConfigProvider auth.ConfigurationProviderWithClaimAccess
|
|
|
|
if p.region == "" {
|
|
logger.Debug("did not get %s from Terraform configuration (ResourcePrincipal), falling back to environment variable", RegionAttrName)
|
|
resourcePrincipalAuthConfigProvider, err = auth.ResourcePrincipalConfigurationProvider()
|
|
} else {
|
|
resourcePrincipalAuthConfigProvider, err = auth.ResourcePrincipalConfigurationProviderForRegion(common.StringToRegion(p.region))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configProviders = append(configProviders, resourcePrincipalAuthConfigProvider)
|
|
case strings.ToLower(AuthOKEWorkloadIdentity):
|
|
logger.Info("Attempting to authenticate using OKE workload identity")
|
|
okeWorkloadIdentityConfigProvider, err := auth.OkeWorkloadIdentityConfigurationProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can not get oke workload indentity based auth config provider %v", err)
|
|
}
|
|
configProviders = append(configProviders, okeWorkloadIdentityConfigProvider)
|
|
default:
|
|
return nil, fmt.Errorf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity)
|
|
}
|
|
|
|
return configProviders, nil
|
|
}
|
|
func (p ociAuthConfigProvider) getSdkConfigProvider() (common.ConfigurationProvider, error) {
|
|
|
|
configProviders, err := p.getConfigProviders()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configProviders = append(configProviders, p)
|
|
//In GoSDK, the first step is to check if AuthType exists,
|
|
//for composite provider, we only check the first provider in the list for the AuthType.
|
|
//Then SDK will based on the AuthType to Create the actual provider if it's a valid value.
|
|
//If not, then SDK will base on the order in the composite provider list to check for necessary info (tenancyid, userID, fingerprint, region, keyID).
|
|
if p.configFileProfile == "" {
|
|
configProviders = append(configProviders, common.DefaultConfigProvider())
|
|
} else {
|
|
defaultPath := path.Join(getHomeFolder(), DefaultConfigDirName, DefaultConfigFileName)
|
|
err := checkProfile(p.configFileProfile, defaultPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configProviders = append(configProviders, common.CustomProfileConfigProvider(defaultPath, p.configFileProfile))
|
|
}
|
|
sdkConfigProvider, err := common.ComposingConfigurationProvider(configProviders)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return sdkConfigProvider, nil
|
|
}
|
|
func buildHttpClient() (httpClient *http.Client) {
|
|
httpClient = &http.Client{
|
|
Timeout: getDurationFromEnvVar(HTTPRequestTimeOut, DefaultRequestTimeout),
|
|
Transport: &http.Transport{
|
|
DialContext: (&net.Dialer{
|
|
Timeout: getDurationFromEnvVar(DialContextConnectionTimeout, DefaultConnectionTimeout),
|
|
}).DialContext,
|
|
TLSHandshakeTimeout: getDurationFromEnvVar(TLSHandshakeTimeout, DefaultTLSHandshakeTimeout),
|
|
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
|
|
Proxy: http.ProxyFromEnvironment,
|
|
},
|
|
}
|
|
return
|
|
}
|
|
|
|
func getCertificateFileBytes(certificateFileFullPath string) (pemRaw []byte, err error) {
|
|
absFile, err := filepath.Abs(certificateFileFullPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't form absolute path of %s: %v", certificateFileFullPath, err)
|
|
}
|
|
|
|
if pemRaw, err = os.ReadFile(absFile); err != nil {
|
|
return nil, fmt.Errorf("can't read %s: %v", certificateFileFullPath, err)
|
|
}
|
|
return
|
|
}
|
|
func UserAgentFromEnv() string {
|
|
|
|
userAgentFromEnv := getEnvSettingWithBlankDefault(UserAgentSDKNameEnv)
|
|
if userAgentFromEnv == "" {
|
|
userAgentFromEnv = getEnvSettingWithBlankDefault(UserAgentTerraformNameEnv)
|
|
}
|
|
if userAgentFromEnv == "" {
|
|
userAgentFromEnv = DefaultUserAgentBackendName
|
|
}
|
|
|
|
return userAgentFromEnv
|
|
}
|
|
|
|
// OboTokenProvider interface that wraps information about auth tokens so the sdk client can make calls
|
|
// on behalf of a different authorized user
|
|
type OboTokenProvider interface {
|
|
OboToken() (string, error)
|
|
}
|
|
|
|
// EmptyOboTokenProvider always provides an empty obo token
|
|
type emptyOboTokenProvider struct{}
|
|
|
|
// OboToken provides the obo token
|
|
func (provider emptyOboTokenProvider) OboToken() (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
type oboTokenProviderFromEnv struct{}
|
|
|
|
func (p oboTokenProviderFromEnv) OboToken() (string, error) {
|
|
// priority token from path than token from environment
|
|
if tokenPath := getEnvSettingWithBlankDefault(OboTokenPath); tokenPath != "" {
|
|
tokenByte, err := os.ReadFile(tokenPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(tokenByte), nil
|
|
}
|
|
return getEnvSettingWithBlankDefault(OboTokenAttrName), nil
|
|
}
|
|
func buildConfigureClient(configProvider common.ConfigurationProvider, httpClient *http.Client) (*objectstorage.ObjectStorageClient, error) {
|
|
|
|
userAgentName := UserAgentFromEnv()
|
|
userAgent := fmt.Sprintf(UserAgentFormatter, common.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH, version.String(), userAgentName)
|
|
|
|
useOboToken, err := strconv.ParseBool(getEnvSettingWithDefault("use_obo_token", "false"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
setSDKLogger()
|
|
client, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(configProvider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hostOverride := getClientHostOverride(); hostOverride != "" {
|
|
client.Host = hostOverride
|
|
}
|
|
requestSigner := common.DefaultRequestSigner(configProvider)
|
|
var oboTokenProvider OboTokenProvider
|
|
oboTokenProvider = emptyOboTokenProvider{}
|
|
if useOboToken {
|
|
// Add Obo token to the default list and Update the signer
|
|
httpHeadersToSign := append(common.DefaultGenericHeaders(), RequestHeaderOpcOboToken)
|
|
requestSigner = common.RequestSigner(configProvider, httpHeadersToSign, common.DefaultBodyHeaders())
|
|
oboTokenProvider = oboTokenProviderFromEnv{}
|
|
}
|
|
|
|
client.HTTPClient = httpClient
|
|
client.UserAgent = userAgent
|
|
client.Signer = requestSigner
|
|
client.Interceptor = func(r *http.Request) error {
|
|
if oboToken, err := oboTokenProvider.OboToken(); err == nil && oboToken != "" {
|
|
r.Header.Set(RequestHeaderOpcOboToken, oboToken)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
domainNameOverride := getEnvSettingWithBlankDefault(DomainNameOverrideEnv)
|
|
|
|
if domainNameOverride != "" {
|
|
hasCorrectDomainName := getEnvSettingWithBlankDefault(HasCorrectDomainNameEnv)
|
|
re := regexp.MustCompile(`(.*?)[-\w]+\.\w+$`) // (capture: preamble) match: d0main-name . tld end-of-string
|
|
if hasCorrectDomainName == "" || !strings.HasSuffix(client.Host, hasCorrectDomainName) {
|
|
client.Host = re.ReplaceAllString(client.Host, "${1}"+domainNameOverride) // non-match conveniently returns original string
|
|
}
|
|
}
|
|
|
|
customCertLoc := getEnvSettingWithBlankDefault(CustomCertLocationEnv)
|
|
|
|
if customCertLoc != "" {
|
|
cert, err := os.ReadFile(customCertLoc)
|
|
if err != nil {
|
|
return &client, err
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if ok := pool.AppendCertsFromPEM(cert); !ok {
|
|
return nil, fmt.Errorf("failed to append custom cert to the pool")
|
|
}
|
|
// install the certificates in the client
|
|
httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = pool
|
|
}
|
|
|
|
if acceptLocalCerts := getEnvSettingWithBlankDefault(AcceptLocalCerts); acceptLocalCerts != "" {
|
|
if boolVal, err := strconv.ParseBool(acceptLocalCerts); err == nil {
|
|
httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = boolVal
|
|
}
|
|
}
|
|
|
|
return &client, nil
|
|
}
|
|
func getClientHostOverride() string {
|
|
// Get the host URL override for clients
|
|
|
|
clientHostOverridesString := getEnvSettingWithBlankDefault(ClientHostOverridesEnv)
|
|
if clientHostOverridesString == "" {
|
|
return ""
|
|
}
|
|
|
|
clientHostFlags := strings.Split(clientHostOverridesString, ColonDelimiter)
|
|
for _, item := range clientHostFlags {
|
|
clientNameHost := strings.Split(item, EqualToOperatorDelimiter)
|
|
if clientNameHost == nil || len(clientNameHost) != 2 {
|
|
continue
|
|
}
|
|
if clientNameHost[0] == ObjectStorageClientName {
|
|
return clientNameHost[1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|