blob: 055efb92400cd565cc20ee5d7be49691baf78b83 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// 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 ""
}