diff --git a/cmd/cloudFoundryDeploy.go b/cmd/cloudFoundryDeploy.go index e7cb683ea2..d2471330f2 100644 --- a/cmd/cloudFoundryDeploy.go +++ b/cmd/cloudFoundryDeploy.go @@ -602,6 +602,8 @@ func cfDeploy( CfSpace: config.Space, Username: config.Username, Password: config.Password, + ClientID: config.ClientID, + ClientSecret: config.ClientSecret, CfLoginOpts: strings.Fields(config.LoginParameters), }) } diff --git a/cmd/cloudFoundryDeploy_generated.go b/cmd/cloudFoundryDeploy_generated.go index b72e95e824..e0857c7196 100644 --- a/cmd/cloudFoundryDeploy_generated.go +++ b/cmd/cloudFoundryDeploy_generated.go @@ -45,6 +45,8 @@ type cloudFoundryDeployOptions struct { Password string `json:"password,omitempty"` Space string `json:"space,omitempty"` Username string `json:"username,omitempty"` + ClientID string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` } type cloudFoundryDeployInflux struct { @@ -156,6 +158,8 @@ The step achieves this via following deploy tools log.RegisterSecret(stepConfig.DockerUsername) log.RegisterSecret(stepConfig.Password) log.RegisterSecret(stepConfig.Username) + log.RegisterSecret(stepConfig.ClientID) + log.RegisterSecret(stepConfig.ClientSecret) if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) @@ -269,15 +273,15 @@ func addCloudFoundryDeployFlags(cmd *cobra.Command, stepConfig *cloudFoundryDepl cmd.Flags().StringVar(&stepConfig.MtaPath, "mtaPath", os.Getenv("PIPER_mtaPath"), "Defines the path to *.mtar for deployment with the mtaDeployPlugin") cmd.Flags().StringVar(&stepConfig.Org, "org", os.Getenv("PIPER_org"), "Cloud Foundry target organization.") - cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password") + cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password. Not required when clientId and clientSecret are provided.") cmd.Flags().StringVar(&stepConfig.Space, "space", os.Getenv("PIPER_space"), "Cloud Foundry target space") - cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User name used for deployment") + cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User name used for deployment. Not required when clientId and clientSecret are provided.") + cmd.Flags().StringVar(&stepConfig.ClientID, "clientId", os.Getenv("PIPER_clientId"), "Client ID for XSUAA client credentials authentication (`cf auth --client-credentials`). When both clientId and clientSecret are provided, username and password are not required.") + cmd.Flags().StringVar(&stepConfig.ClientSecret, "clientSecret", os.Getenv("PIPER_clientSecret"), "Client Secret for XSUAA client credentials authentication. When both clientId and clientSecret are provided, username and password are not required.") cmd.MarkFlagRequired("apiEndpoint") cmd.MarkFlagRequired("org") - cmd.MarkFlagRequired("password") cmd.MarkFlagRequired("space") - cmd.MarkFlagRequired("username") } // retrieve step metadata @@ -550,7 +554,7 @@ func cloudFoundryDeployMetadata() config.StepData { }, Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, Type: "string", - Mandatory: true, + Mandatory: false, Aliases: []config.Alias{}, Default: os.Getenv("PIPER_password"), }, @@ -580,10 +584,40 @@ func cloudFoundryDeployMetadata() config.StepData { }, Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, Type: "string", - Mandatory: true, + Mandatory: false, Aliases: []config.Alias{}, Default: os.Getenv("PIPER_username"), }, + { + Name: "clientId", + ResourceRef: []config.ResourceReference{ + { + Name: "cloudfoundryVaultSecretName", + Type: "vaultSecret", + Default: "cloudfoundry-$(org)-$(space)", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_clientId"), + }, + { + Name: "clientSecret", + ResourceRef: []config.ResourceReference{ + { + Name: "cloudfoundryVaultSecretName", + Type: "vaultSecret", + Default: "cloudfoundry-$(org)-$(space)", + }, + }, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_clientSecret"), + }, }, }, Containers: []config.Container{ diff --git a/pkg/cloudfoundry/Authentication.go b/pkg/cloudfoundry/Authentication.go index 014a829909..f7a70b3f9f 100644 --- a/pkg/cloudfoundry/Authentication.go +++ b/pkg/cloudfoundry/Authentication.go @@ -15,7 +15,9 @@ func (cf *CFUtils) LoginCheck(options LoginOptions) (bool, error) { } // Login logs user in to Cloud Foundry via cf cli. -// Checks if user is logged in first, if not perform 'cf login' command with appropriate parameters +// Checks if user is logged in first, if not perform 'cf login' command with appropriate parameters. +// If ClientID and ClientSecret are provided, client credentials flow is used instead: +// cf api → cf auth --client-credentials → cf target func (cf *CFUtils) Login(options LoginOptions) error { var err error @@ -25,8 +27,17 @@ func (cf *CFUtils) Login(options LoginOptions) error { _c = &command.Command{} } - if options.CfAPIEndpoint == "" || options.CfOrg == "" || options.CfSpace == "" || options.Username == "" || options.Password == "" { - return fmt.Errorf("Failed to login to Cloud Foundry: %w", errors.New("Parameters missing. Please provide the Cloud Foundry Endpoint, Org, Space, Username and Password")) + // Decide authentication flow + useClientCredentials := options.ClientID != "" && options.ClientSecret != "" + + if useClientCredentials { + if options.CfAPIEndpoint == "" || options.CfOrg == "" || options.CfSpace == "" { + return fmt.Errorf("Failed to login to Cloud Foundry: %w", errors.New("Parameters missing. Please provide the Cloud Foundry Endpoint, Org and Space for client credentials login")) + } + } else { + if options.CfAPIEndpoint == "" || options.CfOrg == "" || options.CfSpace == "" || options.Username == "" || options.Password == "" { + return fmt.Errorf("Failed to login to Cloud Foundry: %w", errors.New("Parameters missing. Please provide the Cloud Foundry Endpoint, Org, Space, Username and Password")) + } } var loggedIn bool @@ -39,19 +50,31 @@ func (cf *CFUtils) Login(options LoginOptions) error { if err == nil { log.Entry().Info("Logging in to Cloud Foundry") - - var cfLoginScript = append([]string{ - "login", - "-a", options.CfAPIEndpoint, - "-o", options.CfOrg, - "-s", options.CfSpace, - "-u", options.Username, - "-p", options.Password, - }, options.CfLoginOpts...) - log.Entry().WithField("cfAPI:", options.CfAPIEndpoint).WithField("cfOrg", options.CfOrg).WithField("space", options.CfSpace).Info("Logging into Cloud Foundry..") - err = _c.RunExecutable("cf", cfLoginScript...) + if useClientCredentials { + // Step 1: set API endpoint + err = _c.RunExecutable("cf", "api", options.CfAPIEndpoint) + if err == nil { + // Step 2: authenticate with client credentials + authArgs := append([]string{"auth", options.ClientID, options.ClientSecret, "--client-credentials"}, options.CfLoginOpts...) + err = _c.RunExecutable("cf", authArgs...) + } + if err == nil { + // Step 3: target org and space + err = _c.RunExecutable("cf", "target", "-o", options.CfOrg, "-s", options.CfSpace) + } + } else { + var cfLoginScript = append([]string{ + "login", + "-a", options.CfAPIEndpoint, + "-o", options.CfOrg, + "-s", options.CfSpace, + "-u", options.Username, + "-p", options.Password, + }, options.CfLoginOpts...) + err = _c.RunExecutable("cf", cfLoginScript...) + } } if err != nil { @@ -92,7 +115,11 @@ type LoginOptions struct { CfSpace string Username string Password string - CfLoginOpts []string + // ClientID and ClientSecret enable XSUAA client credentials authentication. + // When both are set, 'cf auth --client-credentials' is used instead of 'cf login'. + ClientID string + ClientSecret string + CfLoginOpts []string } // CFUtils ... diff --git a/resources/metadata/cloudFoundryDeploy.yaml b/resources/metadata/cloudFoundryDeploy.yaml index 5d6a96dc40..6b427f6b20 100644 --- a/resources/metadata/cloudFoundryDeploy.yaml +++ b/resources/metadata/cloudFoundryDeploy.yaml @@ -331,12 +331,12 @@ spec: secret: false - name: password type: string - description: "Password" + description: "Password. Not required when clientId and clientSecret are provided." scope: - PARAMETERS - STAGES - STEPS - mandatory: true + mandatory: false secret: true resourceRef: - name: cfCredentialsId @@ -360,12 +360,12 @@ spec: mandatory: true - name: username type: string - description: User name used for deployment + description: User name used for deployment. Not required when clientId and clientSecret are provided. scope: - PARAMETERS - STAGES - STEPS - mandatory: true + mandatory: false secret: true resourceRef: - name: cfCredentialsId @@ -374,6 +374,34 @@ spec: - type: vaultSecret default: cloudfoundry-$(org)-$(space) name: cloudfoundryVaultSecretName + - name: clientId + type: string + description: "Client ID for XSUAA client credentials authentication (`cf auth --client-credentials`). + When both clientId and clientSecret are provided, username and password are not required." + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: false + secret: true + resourceRef: + - type: vaultSecret + default: cloudfoundry-$(org)-$(space) + name: cloudfoundryVaultSecretName + - name: clientSecret + type: string + description: "Client Secret for XSUAA client credentials authentication. + When both clientId and clientSecret are provided, username and password are not required." + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: false + secret: true + resourceRef: + - type: vaultSecret + default: cloudfoundry-$(org)-$(space) + name: cloudfoundryVaultSecretName containers: - name: cfDeploy image: ppiper/cf-cli:latest