// Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved. // This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. package auth import ( "context" "crypto/rsa" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/nrdcg/oci-go-sdk/common/v1065" ) type resourcePrincipalV3Client struct { securityToken securityToken mux sync.Mutex sessionKeySupplier sessionKeySupplier rptUrl string rpstUrl string leafResourcePrincipalKeyProvider ConfigurationProviderWithClaimAccess //ResourcePrincipalTargetServiceClient client that calls the target service to acquire a resource principal token //ResourcePrincipalTargetServiceClient common.BaseClient //ResourcePrincipalSessionTokenClient. The client used to communicate with identity to exchange a resource principal for // resource principal session token //ResourcePrincipalSessionTokenClient common.BaseClient } // acquireResourcePrincipalToken acquires the resource principal from the target service func (c *resourcePrincipalV3Client) acquireResourcePrincipalToken(rptClient common.BaseClient, path string, signer common.HTTPRequestSigner) (tokenResponse resourcePrincipalTokenResponse, parentRptURL string, err error) { rpServiceClient := rptClient rpServiceClient.Signer = signer //Create a request with the instanceId request := common.MakeDefaultHTTPRequest(http.MethodGet, path) //Call the target service response, err := rpServiceClient.Call(context.Background(), &request) if err != nil { return } defer common.CloseBodyIfValid(response) // Extract the opc-parent-rpt-url header value parentRptURL = response.Header.Get(OpcParentRptUrlHeader) tokenResponse = resourcePrincipalTokenResponse{} err = common.UnmarshalResponse(response, &tokenResponse) return } // exchangeToken exchanges a resource principal token from the target service with a session token from identity func (c *resourcePrincipalV3Client) exchangeToken(rpstClient common.BaseClient, signer common.HTTPRequestSigner, publicKeyBase64 string, tokenResponse resourcePrincipalTokenResponse) (sessionToken string, err error) { rpServiceClient := rpstClient rpServiceClient.Signer = signer // Call identity service to get resource principal session token sessionTokenReq := resourcePrincipalSessionTokenRequest{ resourcePrincipalSessionTokenRequestBody{ ServicePrincipalSessionToken: tokenResponse.Body.ServicePrincipalSessionToken, ResourcePrincipalToken: tokenResponse.Body.ResourcePrincipalToken, SessionPublicKey: publicKeyBase64, }, } sessionTokenHTTPReq, err := common.MakeDefaultHTTPRequestWithTaggedStruct(http.MethodPost, "", sessionTokenReq) if err != nil { return } sessionTokenHTTPRes, err := rpServiceClient.Call(context.Background(), &sessionTokenHTTPReq) if err != nil { return } defer common.CloseBodyIfValid(sessionTokenHTTPRes) sessionTokenRes := x509FederationResponse{} err = common.UnmarshalResponse(sessionTokenHTTPRes, &sessionTokenRes) if err != nil { return } sessionToken = sessionTokenRes.Token.Token return } // getSecurityToken makes the appropriate calls to acquire a resource principal security token func (c *resourcePrincipalV3Client) getSecurityToken() (securityToken, error) { //c.leafResourcePrincipalKeyProvider.KeyID() //common.Debugf("Refreshing resource principal token") //Read the public key from the session supplier. pem := c.sessionKeySupplier.PublicKeyPemRaw() pemSanitized := sanitizeCertificateString(string(pem)) return c.getSecurityTokenWithDepth(c.leafResourcePrincipalKeyProvider, 1, c.rptUrl, pemSanitized) } func (c *resourcePrincipalV3Client) getSecurityTokenWithDepth(keyProvider ConfigurationProviderWithClaimAccess, depth int, rptUrl, publicKey string) (securityToken, error) { //Build the target service client rpTargetServiceClient, err := common.NewClientWithConfig(keyProvider) if err != nil { return nil, err } rpTokenURL, err := url.Parse(rptUrl) if err != nil { return nil, err } common.Debugf("rptURL: %v", rpTokenURL) rpTargetServiceClient.Host = rpTokenURL.Scheme + "://" + rpTokenURL.Host //Build the identity client for token service rpTokenSessionClient, err := common.NewClientWithConfig(keyProvider) if err != nil { return nil, err } // Set RPST endpoint if passed in from env var, otherwise create it from region if c.rpstUrl != "" { rpSessionTokenURL, err := url.Parse(c.rpstUrl) if err != nil { return nil, err } rpTokenSessionClient.Host = rpSessionTokenURL.Scheme + "://" + rpSessionTokenURL.Host } else { regionStr, err := c.leafResourcePrincipalKeyProvider.Region() if err != nil { return nil, fmt.Errorf("missing RPST env var and cannot determine region: %v", err) } region := common.StringToRegion(regionStr) rpTokenSessionClient.Host = fmt.Sprintf("https://%s", region.Endpoint("auth")) } rpTokenSessionClient.BasePath = identityResourcePrincipalSessionTokenPath //Acquire resource principal token from target service common.Debugf("Acquiring resource principal token from target service") tokenResponse, parentRptURL, err := c.acquireResourcePrincipalToken(rpTargetServiceClient, rpTokenURL.Path, common.DefaultRequestSigner(keyProvider)) if err != nil { return nil, err } //Exchange resource principal token for session token from identity common.Debugf("Exchanging resource principal token for resource principal session token") sessionToken, err := c.exchangeToken(rpTokenSessionClient, common.DefaultRequestSigner(keyProvider), publicKey, tokenResponse) if err != nil { return nil, err } // Base condition for recursion // return the security token obtained last in the following cases // 1. if depth is more than 10 // 2. if opc-parent-rpt-url header is not passed or is empty // 3. if opc-parent-rpt-url matches the last rpt url if depth >= 10 || parentRptURL == "" || strings.EqualFold(parentRptURL, rptUrl) { return newPrincipalToken(sessionToken) } fd, err := newStaticFederationClient(sessionToken, c.sessionKeySupplier) if err != nil { err := fmt.Errorf("can not create resource principal, due to: %s ", err.Error()) return nil, resourcePrincipalError{err: err} } region, _ := keyProvider.Region() configProviderForNextCall := resourcePrincipalKeyProvider{ fd, common.Region(region), } return c.getSecurityTokenWithDepth(&configProviderForNextCall, depth+1, parentRptURL, publicKey) } func (c *resourcePrincipalV3Client) renewSecurityToken() (err error) { if err = c.sessionKeySupplier.Refresh(); err != nil { return fmt.Errorf("failed to refresh session key: %s", err.Error()) } common.Logf("Renewing security token at: %v\n", time.Now().Format("15:04:05.000")) if c.securityToken, err = c.getSecurityToken(); err != nil { return fmt.Errorf("failed to get security token: %s", err.Error()) } common.Logf("Security token renewed at: %v\n", time.Now().Format("15:04:05.000")) return nil } func (c *resourcePrincipalV3Client) renewSecurityTokenIfNotValid() (err error) { if c.securityToken == nil || !c.securityToken.Valid() { if err = c.renewSecurityToken(); err != nil { return fmt.Errorf("failed to renew resource principal security token: %s", err.Error()) } } return nil } func (c *resourcePrincipalV3Client) PrivateKey() (*rsa.PrivateKey, error) { c.mux.Lock() defer c.mux.Unlock() if err := c.renewSecurityTokenIfNotValid(); err != nil { return nil, err } return c.sessionKeySupplier.PrivateKey(), nil } func (c *resourcePrincipalV3Client) SecurityToken() (token string, err error) { c.mux.Lock() defer c.mux.Unlock() if err = c.renewSecurityTokenIfNotValid(); err != nil { return "", err } return c.securityToken.String(), nil } type resourcePrincipalKeyProviderV3 struct { resourcePrincipalClient resourcePrincipalV3Client } type resourcePrincipalV30ConfigurationProvider struct { keyProvider resourcePrincipalKeyProviderV3 region *common.Region } func (r *resourcePrincipalV30ConfigurationProvider) Refreshable() bool { return true } func (r *resourcePrincipalV30ConfigurationProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { privateKey, err := r.keyProvider.resourcePrincipalClient.PrivateKey() if err != nil { err = fmt.Errorf("failed to get resource principal private key: %s", err.Error()) return nil, err } return privateKey, nil } func (r *resourcePrincipalV30ConfigurationProvider) KeyID() (string, error) { var securityToken string var err error if securityToken, err = r.keyProvider.resourcePrincipalClient.SecurityToken(); err != nil { return "", fmt.Errorf("failed to get resource principal security token: %s", err.Error()) } return fmt.Sprintf("ST$%s", securityToken), nil } func (r *resourcePrincipalV30ConfigurationProvider) TenancyOCID() (string, error) { return r.keyProvider.resourcePrincipalClient.leafResourcePrincipalKeyProvider.TenancyOCID() } func (r *resourcePrincipalV30ConfigurationProvider) UserOCID() (string, error) { return "", nil } func (r *resourcePrincipalV30ConfigurationProvider) KeyFingerprint() (string, error) { return "", nil } func (r *resourcePrincipalV30ConfigurationProvider) Region() (string, error) { if r.region == nil { common.Debugf("Region in resource principal configuration provider v30 is nil.") return "", nil } return string(*r.region), nil } func (r *resourcePrincipalV30ConfigurationProvider) AuthType() (common.AuthConfig, error) { return common.AuthConfig{common.UnknownAuthenticationType, false, nil}, fmt.Errorf("unsupported, keep the interface") } func (r *resourcePrincipalV30ConfigurationProvider) GetClaim(key string) (interface{}, error) { //TODO implement me panic("implement me") } type resourcePrincipalV30ConfiguratorBuilder struct { leafResourcePrincipalKeyProvider ConfigurationProviderWithClaimAccess rptUrlForParent, rpstUrlForParent *string } // ResourcePrincipalV3ConfiguratorBuilder creates a new resourcePrincipalV30ConfiguratorBuilder. func ResourcePrincipalV3ConfiguratorBuilder(leafResourcePrincipalKeyProvider ConfigurationProviderWithClaimAccess) *resourcePrincipalV30ConfiguratorBuilder { return &resourcePrincipalV30ConfiguratorBuilder{ leafResourcePrincipalKeyProvider: leafResourcePrincipalKeyProvider, } } // WithParentRPTURL sets the rptUrlForParent field. func (b *resourcePrincipalV30ConfiguratorBuilder) WithParentRPTURL(rptUrlForParent string) *resourcePrincipalV30ConfiguratorBuilder { b.rptUrlForParent = &rptUrlForParent return b } // WithParentRPSTURL sets the rpstUrlForParent field. func (b *resourcePrincipalV30ConfiguratorBuilder) WithParentRPSTURL(rpstUrlForParent string) *resourcePrincipalV30ConfiguratorBuilder { b.rpstUrlForParent = &rpstUrlForParent return b } // Build creates a ConfigurationProviderWithClaimAccess based on the configured values. func (b *resourcePrincipalV30ConfiguratorBuilder) Build() (ConfigurationProviderWithClaimAccess, error) { if b.rptUrlForParent == nil { err := fmt.Errorf("can not create resource principal, environment variable: %s, not present", ResourcePrincipalRptURLForParent) return nil, resourcePrincipalError{err: err} } if b.rpstUrlForParent == nil { common.Debugf("Environment variable %s not present, setting to empty string", ResourcePrincipalRpstEndpointForParent) *b.rpstUrlForParent = "" } rpFedClient := resourcePrincipalV3Client{} rpFedClient.rptUrl = *b.rptUrlForParent rpFedClient.rpstUrl = *b.rpstUrlForParent rpFedClient.sessionKeySupplier = newSessionKeySupplier() rpFedClient.leafResourcePrincipalKeyProvider = b.leafResourcePrincipalKeyProvider region, _ := b.leafResourcePrincipalKeyProvider.Region() return &resourcePrincipalV30ConfigurationProvider{ keyProvider: resourcePrincipalKeyProviderV3{rpFedClient}, region: (*common.Region)(®ion), }, nil } // ResourcePrincipalConfigurationProviderV3 ResourcePrincipalConfigurationProvider is a function that creates and configures a resource principal. func ResourcePrincipalConfigurationProviderV3(leafResourcePrincipalKeyProvider ConfigurationProviderWithClaimAccess) (ConfigurationProviderWithClaimAccess, error) { builder := ResourcePrincipalV3ConfiguratorBuilder(leafResourcePrincipalKeyProvider) builder.rptUrlForParent = requireEnv(ResourcePrincipalRptURLForParent) builder.rpstUrlForParent = requireEnv(ResourcePrincipalRpstEndpointForParent) return builder.Build() }