Skip to content

Commit

Permalink
Move functions to QueryConfig members
Browse files Browse the repository at this point in the history
  • Loading branch information
typeid committed Nov 27, 2023
1 parent a2ee797 commit b20b926
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 200 deletions.
184 changes: 178 additions & 6 deletions cmd/ocm-backplane/cloud/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sts"
ocmsdk "github.com/openshift-online/ocm-sdk-go"
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
BackplaneApi "github.com/openshift/backplane-api/pkg/client"
"github.com/openshift/backplane-cli/pkg/awsutil"
"github.com/openshift/backplane-cli/pkg/cli/config"
bpCredentials "github.com/openshift/backplane-cli/pkg/credentials"
"github.com/openshift/backplane-cli/pkg/utils"
logger "github.com/sirupsen/logrus"
)

const OldFlowSupportRole = "role/RH-Technical-Support-Access"
Expand All @@ -30,6 +33,175 @@ var AssumeRoleSequence = awsutil.AssumeRoleSequence
type QueryConfig struct {
config.BackplaneConfiguration
OcmConnection *ocmsdk.Connection
Cluster *cmv1.Cluster
}

// GetAWSV2Config allows consumers to get an aws-sdk-go-v2 Config to programmatically access the AWS API
func (cfg *QueryConfig) GetAWSV2Config() (aws.Config, error) {
if cfg.Cluster.CloudProvider().ID() != "aws" {
return aws.Config{}, fmt.Errorf("only supported for the aws cloud provider, this cluster has: %s", cfg.Cluster.CloudProvider().ID())
}
creds, err := cfg.GetCloudCredentials()
if err != nil {
return aws.Config{}, err
}

awsCreds, ok := creds.(*bpCredentials.AWSCredentialsResponse)
if !ok {
return aws.Config{}, errors.New("unexpected error: failed to convert backplane creds to AWSCredentialsResponse")
}

return awsCreds.AWSV2Config()
}

// GetCloudCredentials returns Cloud Credentials Response
func (cfg *QueryConfig) GetCloudConsole() (*ConsoleResponse, error) {
ocmToken, _, err := cfg.OcmConnection.Tokens()
if err != nil {
return nil, fmt.Errorf("unable to get token for ocm connection")
}

isolatedBackplane, err := isIsolatedBackplaneAccess(cfg.Cluster, cfg.OcmConnection)
if err != nil {
logger.Infof("failed to determine if the cluster is using isolated backplane access: %v", err)
logger.Infof("for more information, try ocm get /api/clusters_mgmt/v1/clusters/%s/sts_support_jump_role", cfg.Cluster.ID())
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
}

if isolatedBackplane {
logger.Debugf("cluster is using isolated backplane")
targetCredentials, err := cfg.getIsolatedCredentials(ocmToken)
if err != nil {
// TODO: This fallback should be removed in the future
// TODO: when we are more confident in our ability to access clusters using the isolated flow
logger.Infof("failed to assume role with isolated backplane flow: %v", err)
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
return cfg.getCloudConsoleFromPublicAPI(ocmToken)
}

resp, err := awsutil.GetSigninToken(targetCredentials, cfg.Cluster.Region().ID())
if err != nil {
return nil, fmt.Errorf("failed to get signin token: %w", err)
}

signinFederationURL, err := awsutil.GetConsoleURL(resp.SigninToken, cfg.Cluster.Region().ID())
if err != nil {
return nil, fmt.Errorf("failed to generate console url: %w", err)
}
return &ConsoleResponse{ConsoleLink: signinFederationURL.String()}, nil
}

return cfg.getCloudConsoleFromPublicAPI(ocmToken)
}

// GetCloudConsole returns console response calling to public Backplane API
func (cfg *QueryConfig) getCloudConsoleFromPublicAPI(ocmToken string) (*ConsoleResponse, error) {
logger.Debugln("Getting Cloud Console")

client, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, ocmToken, cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return nil, err
}
resp, err := client.GetCloudConsole(context.TODO(), cfg.Cluster.ID())
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, utils.TryPrintAPIError(resp, false)
}

credsResp, err := BackplaneApi.ParseGetCloudConsoleResponse(resp)
if err != nil {
return nil, fmt.Errorf("unable to parse response body from backplane:\n Status Code: %d", resp.StatusCode)
}

if len(credsResp.Body) == 0 {
return nil, fmt.Errorf("empty response from backplane")
}

cliResp := &ConsoleResponse{}
cliResp.ConsoleLink = *credsResp.JSON200.ConsoleLink

return cliResp, nil
}

// GetCloudCredentials returns Cloud Credentials Response
func (cfg *QueryConfig) GetCloudCredentials() (bpCredentials.Response, error) {
ocmToken, _, err := cfg.OcmConnection.Tokens()
if err != nil {
return nil, fmt.Errorf("unable to get token for ocm connection")
}

isolatedBackplane, err := isIsolatedBackplaneAccess(cfg.Cluster, cfg.OcmConnection)
if err != nil {
logger.Infof("failed to determine if the cluster is using isolated backplane access: %v", err)
logger.Infof("for more information, try ocm get /api/clusters_mgmt/v1/clusters/%s/sts_support_jump_role", cfg.Cluster.ID())
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
}

if isolatedBackplane {
logger.Debugf("cluster is using isolated backplane")
targetCredentials, err := cfg.getIsolatedCredentials(ocmToken)
if err != nil {
// TODO: This fallback should be removed in the future
// TODO: when we are more confident in our ability to access clusters using the isolated flow
logger.Infof("failed to assume role with isolated backplane flow: %v", err)
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
return cfg.getCloudCredentialsFromBackplaneAPI(ocmToken)
}

return &bpCredentials.AWSCredentialsResponse{
AccessKeyID: targetCredentials.AccessKeyID,
SecretAccessKey: targetCredentials.SecretAccessKey,
SessionToken: targetCredentials.SessionToken,
Expiration: targetCredentials.Expires.String(),
Region: cfg.Cluster.Region().ID(),
}, nil
}

return cfg.getCloudCredentialsFromBackplaneAPI(ocmToken)
}

func (cfg *QueryConfig) getCloudCredentialsFromBackplaneAPI(ocmToken string) (bpCredentials.Response, error) {
client, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, ocmToken, cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return nil, err
}

resp, err := client.GetCloudCredentials(context.TODO(), cfg.Cluster.ID())
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, utils.TryPrintAPIError(resp, false)
}

logger.Debugln("Parsing response")

credsResp, err := BackplaneApi.ParseGetCloudCredentialsResponse(resp)
if err != nil {
return nil, fmt.Errorf("unable to parse response body from backplane:\n Status Code: %d : err: %v", resp.StatusCode, err)
}

switch cfg.Cluster.CloudProvider().ID() {
case "aws":
cliResp := &bpCredentials.AWSCredentialsResponse{}
if err := json.Unmarshal([]byte(*credsResp.JSON200.Credentials), cliResp); err != nil {
return nil, fmt.Errorf("unable to unmarshal AWS credentials response from backplane %s: %w", *credsResp.JSON200.Credentials, err)
}
cliResp.Region = cfg.Cluster.Region().ID()
return cliResp, nil
case "gcp":
cliResp := &bpCredentials.GCPCredentialsResponse{}
if err := json.Unmarshal([]byte(*credsResp.JSON200.Credentials), cliResp); err != nil {
return nil, fmt.Errorf("unable to unmarshal GCP credentials response from backplane %s: %w", *credsResp.JSON200.Credentials, err)
}
return cliResp, nil
default:
return nil, fmt.Errorf("unsupported cloud provider: %s", cfg.Cluster.CloudProvider().ID())
}
}

type assumeChainResponse struct {
Expand All @@ -41,12 +213,12 @@ type namedRoleArn struct {
Arn string `json:"arn"`
}

func getIsolatedCredentials(clusterID string, cfg *QueryConfig, ocmToken *string) (aws.Credentials, error) {
if clusterID == "" {
func (cfg *QueryConfig) getIsolatedCredentials(ocmToken string) (aws.Credentials, error) {
if cfg.Cluster.ID() == "" {
return aws.Credentials{}, errors.New("must provide non-empty cluster ID")
}

email, err := utils.GetStringFieldFromJWT(*ocmToken, "email")
email, err := utils.GetStringFieldFromJWT(ocmToken, "email")
if err != nil {
return aws.Credentials{}, fmt.Errorf("unable to extract email from given token: %w", err)
}
Expand All @@ -60,17 +232,17 @@ func getIsolatedCredentials(clusterID string, cfg *QueryConfig, ocmToken *string
return aws.Credentials{}, fmt.Errorf("failed to create sts client: %w", err)
}

seedCredentials, err := AssumeRoleWithJWT(*ocmToken, cfg.BackplaneConfiguration.AssumeInitialArn, initialClient)
seedCredentials, err := AssumeRoleWithJWT(ocmToken, cfg.BackplaneConfiguration.AssumeInitialArn, initialClient)
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to assume role using JWT: %w", err)
}

backplaneClient, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, *ocmToken, cfg.BackplaneConfiguration.ProxyURL)
backplaneClient, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, ocmToken, cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to create backplane client with access token: %w", err)
}

response, err := backplaneClient.GetAssumeRoleSequence(context.TODO(), clusterID)
response, err := backplaneClient.GetAssumeRoleSequence(context.TODO(), cfg.Cluster.ID())
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to fetch arn sequence: %w", err)
}
Expand Down
35 changes: 26 additions & 9 deletions cmd/ocm-backplane/cloud/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var _ = Describe("getIsolatedCredentials", func() {
testAccessKeyID string
testSecretAccessKey string
testSessionToken string
queryConfig QueryConfig
testQueryConfig QueryConfig
)

BeforeEach(func() {
Expand All @@ -48,24 +48,41 @@ var _ = Describe("getIsolatedCredentials", func() {
testAccessKeyID = "test-access-key-id"
testSecretAccessKey = "test-secret-access-key"
testSessionToken = "test-session-token"
queryConfig = QueryConfig{OcmConnection: &sdk.Connection{}, BackplaneConfiguration: config.BackplaneConfiguration{URL: "test", AssumeInitialArn: "test"}}

stsBuilder := &cmv1.STSBuilder{}
stsBuilder.Enabled(true)

awsBuilder := &cmv1.AWSBuilder{}
awsBuilder.STS(stsBuilder)

clusterBuilder := cmv1.ClusterBuilder{}
clusterBuilder.AWS(awsBuilder)
clusterBuilder.ID(testClusterID)

cluster, _ := clusterBuilder.Build()
testQueryConfig = QueryConfig{OcmConnection: &sdk.Connection{}, BackplaneConfiguration: config.BackplaneConfiguration{URL: "test", AssumeInitialArn: "test"}, Cluster: cluster}
})

AfterEach(func() {
mockCtrl.Finish()
})

Context("Execute getIsolatedCredentials", func() {
It("should fail if no argument is provided", func() {
_, err := getIsolatedCredentials("", &queryConfig, &testOcmToken)
It("should fail if empty cluster ID is provided", func() {
clusterBuilder := cmv1.ClusterBuilder{}
clusterBuilder.ID("")
cluster, _ := clusterBuilder.Build()
testQueryConfig.Cluster = cluster

_, err := testQueryConfig.getIsolatedCredentials(testOcmToken)
Expect(err).To(Equal(fmt.Errorf("must provide non-empty cluster ID")))
})
It("should fail if cannot create sts client with proxy", func() {
StsClient = func(proxyURL *string) (*sts.Client, error) {
return nil, errors.New(":(")
}

_, err := getIsolatedCredentials(testClusterID, &queryConfig, &testOcmToken)
_, err := testQueryConfig.getIsolatedCredentials(testOcmToken)
Expect(err.Error()).To(Equal("failed to create sts client: :("))
})
It("should fail if initial role cannot be assumed with JWT", func() {
Expand All @@ -76,7 +93,7 @@ var _ = Describe("getIsolatedCredentials", func() {
return aws.Credentials{}, errors.New("failure")
}

_, err := getIsolatedCredentials(testClusterID, &queryConfig, &testOcmToken)
_, err := testQueryConfig.getIsolatedCredentials(testOcmToken)
Expect(err.Error()).To(Equal("failed to assume role using JWT: failure"))
})
It("should fail if email cannot be pulled off JWT", func() {
Expand All @@ -93,7 +110,7 @@ var _ = Describe("getIsolatedCredentials", func() {
}, nil
}

_, err := getIsolatedCredentials(testClusterID, &queryConfig, &testOcmToken)
_, err := testQueryConfig.getIsolatedCredentials(testOcmToken)
Expect(err.Error()).To(Equal("unable to extract email from given token: no field email on given token"))
})
It("should fail if error creating backplane api client", func() {
Expand All @@ -110,9 +127,9 @@ var _ = Describe("getIsolatedCredentials", func() {
NewStaticCredentialsProvider = func(key, secret, session string) credentials.StaticCredentialsProvider {
return credentials.StaticCredentialsProvider{}
}
mockClientUtil.EXPECT().GetBackplaneClient(queryConfig.BackplaneConfiguration.URL, testOcmToken, nil).Return(nil, errors.New("foo")).Times(1)
mockClientUtil.EXPECT().GetBackplaneClient(testQueryConfig.BackplaneConfiguration.URL, testOcmToken, nil).Return(nil, errors.New("foo")).Times(1)

_, err := getIsolatedCredentials(testClusterID, &queryConfig, &testOcmToken)
_, err := testQueryConfig.getIsolatedCredentials(testOcmToken)
Expect(err.Error()).To(Equal("failed to create backplane client with access token: foo"))
})
})
Expand Down
Loading

0 comments on commit b20b926

Please sign in to comment.