// 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 "" }