You've already forked sap-jenkins-library
							
							
				mirror of
				https://github.com/SAP/jenkins-library.git
				synced 2025-10-30 23:57:50 +02:00 
			
		
		
		
	Feature/approle secret id rotation (#2311)
* add new step vault secret * add debug log Co-authored-by: Oliver Nocon <33484802+OliverNocon@users.noreply.github.com>
This commit is contained in:
		| @@ -117,6 +117,7 @@ func Execute() { | ||||
| 	rootCmd.AddCommand(AbapAddonAssemblyKitReserveNextPackagesCommand()) | ||||
| 	rootCmd.AddCommand(CloudFoundryCreateSpaceCommand()) | ||||
| 	rootCmd.AddCommand(CloudFoundryDeleteSpaceCommand()) | ||||
| 	rootCmd.AddCommand(VaultRotateSecretIdCommand()) | ||||
|  | ||||
| 	addRootFlags(rootCmd) | ||||
| 	if err := rootCmd.Execute(); err != nil { | ||||
| @@ -266,14 +267,6 @@ func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName strin | ||||
| 		GeneralConfig.NoTelemetry = true | ||||
| 	} | ||||
|  | ||||
| 	if !GeneralConfig.Verbose && stepConfig.Config["verbose"] != nil { | ||||
| 		if verboseValue, ok := stepConfig.Config["verbose"].(bool); ok { | ||||
| 			log.SetVerbose(verboseValue) | ||||
| 		} else { | ||||
| 			return fmt.Errorf("invalid value for parameter verbose: '%v'", stepConfig.Config["verbose"]) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	stepConfig.Config = checkTypes(stepConfig.Config, options) | ||||
| 	confJSON, _ := json.Marshal(stepConfig.Config) | ||||
| 	_ = json.Unmarshal(confJSON, &options) | ||||
|   | ||||
							
								
								
									
										109
									
								
								cmd/vaultRotateSecretId.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								cmd/vaultRotateSecretId.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/api" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/jenkins" | ||||
| 	"github.com/SAP/jenkins-library/pkg/vault" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| ) | ||||
|  | ||||
| type vaultRotateSecretIDUtils interface { | ||||
| 	GetAppRoleSecretIDTtl(secretID, roleName string) (time.Duration, error) | ||||
| 	GetAppRoleName() (string, error) | ||||
| 	GenerateNewAppRoleSecret(secretID string, roleName string) (string, error) | ||||
| 	UpdateSecretInStore(config *vaultRotateSecretIdOptions, secretID string) error | ||||
| 	GetConfig() *vaultRotateSecretIdOptions | ||||
| } | ||||
|  | ||||
| type vaultRotateSecretIDUtilsBundle struct { | ||||
| 	*vault.Client | ||||
| 	config     *vaultRotateSecretIdOptions | ||||
| 	updateFunc func(config *vaultRotateSecretIdOptions, secretID string) error | ||||
| } | ||||
|  | ||||
| func (v vaultRotateSecretIDUtilsBundle) GetConfig() *vaultRotateSecretIdOptions { | ||||
| 	return v.config | ||||
| } | ||||
|  | ||||
| func (v vaultRotateSecretIDUtilsBundle) UpdateSecretInStore(config *vaultRotateSecretIdOptions, secretID string) error { | ||||
| 	return v.updateFunc(config, secretID) | ||||
| } | ||||
|  | ||||
| func vaultRotateSecretId(config vaultRotateSecretIdOptions, telemetryData *telemetry.CustomData) { | ||||
|  | ||||
| 	client, err := vault.NewClientWithAppRole(&vault.Config{ | ||||
| 		Config: &api.Config{ | ||||
| 			Address: config.VaultServerURL, | ||||
| 		}, | ||||
| 		Namespace: config.VaultNamespace, | ||||
| 	}, GeneralConfig.VaultRoleID, GeneralConfig.VaultRoleSecretID) | ||||
|  | ||||
| 	utils := vaultRotateSecretIDUtilsBundle{ | ||||
| 		Client:     &client, | ||||
| 		config:     &config, | ||||
| 		updateFunc: writeVaultSecretIDToStore, | ||||
| 	} | ||||
|  | ||||
| 	err = runVaultRotateSecretID(utils) | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Fatal("step execution failed") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func runVaultRotateSecretID(utils vaultRotateSecretIDUtils) error { | ||||
| 	config := utils.GetConfig() | ||||
|  | ||||
| 	roleName, err := utils.GetAppRoleName() | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Warn("Could not fetch approle role name from vault. Secret ID rotation failed!") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	ttl, err := utils.GetAppRoleSecretIDTtl(GeneralConfig.VaultRoleSecretID, roleName) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		log.Entry().WithError(err).Warn("Could not fetch secret ID TTL. Secret ID rotation failed!") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	log.Entry().Debugf("Your secret ID is about to expire in %.0f", ttl.Round(time.Hour*24).Hours()/24) | ||||
|  | ||||
| 	if ttl > time.Duration(config.DaysBeforeExpiry)*24*time.Hour { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	newSecretID, err := utils.GenerateNewAppRoleSecret(GeneralConfig.VaultRoleSecretID, roleName) | ||||
|  | ||||
| 	if err != nil || newSecretID == "" { | ||||
| 		log.Entry().WithError(err).Warn("Generating a new secret ID failed. Secret ID rotation faield!") | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err = utils.UpdateSecretInStore(config, newSecretID); err != nil { | ||||
| 		log.Entry().WithError(err).Warnf("Could not write secret back to secret store %s", config.SecretStore) | ||||
| 	} | ||||
| 	log.Entry().Infof("Secret has been successfully updated in secret store %s", config.SecretStore) | ||||
| 	return nil | ||||
|  | ||||
| } | ||||
|  | ||||
| func writeVaultSecretIDToStore(config *vaultRotateSecretIdOptions, secretID string) error { | ||||
| 	switch config.SecretStore { | ||||
| 	case "jenkins": | ||||
| 		instance, err := jenkins.Instance(&http.Client{}, config.JenkinsURL, config.JenkinsUsername, config.JenkinsToken) | ||||
| 		if err != nil { | ||||
| 			log.Entry().Warn("Could not write secret ID back to jenkins") | ||||
| 			return err | ||||
| 		} | ||||
| 		credManager := jenkins.NewCredentialsManager(instance) | ||||
| 		credential := jenkins.StringCredentials{ID: config.VaultAppRoleSecretTokenCredentialsID, Secret: secretID} | ||||
| 		return jenkins.UpdateCredential(credManager, config.JenkinsCredentialDomain, credential) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										207
									
								
								cmd/vaultRotateSecretId_generated.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								cmd/vaultRotateSecretId_generated.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | ||||
| // Code generated by piper's step-generator. DO NOT EDIT. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/config" | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/SAP/jenkins-library/pkg/telemetry" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| type vaultRotateSecretIdOptions struct { | ||||
| 	SecretStore                          string `json:"secretStore,omitempty"` | ||||
| 	JenkinsURL                           string `json:"jenkinsUrl,omitempty"` | ||||
| 	JenkinsCredentialDomain              string `json:"jenkinsCredentialDomain,omitempty"` | ||||
| 	JenkinsUsername                      string `json:"jenkinsUsername,omitempty"` | ||||
| 	JenkinsToken                         string `json:"jenkinsToken,omitempty"` | ||||
| 	VaultAppRoleSecretTokenCredentialsID string `json:"vaultAppRoleSecretTokenCredentialsId,omitempty"` | ||||
| 	VaultServerURL                       string `json:"vaultServerUrl,omitempty"` | ||||
| 	VaultNamespace                       string `json:"vaultNamespace,omitempty"` | ||||
| 	DaysBeforeExpiry                     int    `json:"daysBeforeExpiry,omitempty"` | ||||
| } | ||||
|  | ||||
| // VaultRotateSecretIdCommand Rotate vault AppRole Secret ID | ||||
| func VaultRotateSecretIdCommand() *cobra.Command { | ||||
| 	const STEP_NAME = "vaultRotateSecretId" | ||||
|  | ||||
| 	metadata := vaultRotateSecretIdMetadata() | ||||
| 	var stepConfig vaultRotateSecretIdOptions | ||||
| 	var startTime time.Time | ||||
|  | ||||
| 	var createVaultRotateSecretIdCmd = &cobra.Command{ | ||||
| 		Use:   STEP_NAME, | ||||
| 		Short: "Rotate vault AppRole Secret ID", | ||||
| 		Long:  `This step takes the given Vault secret ID and checks whether it needs to be renewed and if so it will update the secret ID in the configured secret store.`, | ||||
| 		PreRunE: func(cmd *cobra.Command, _ []string) error { | ||||
| 			startTime = time.Now() | ||||
| 			log.SetStepName(STEP_NAME) | ||||
| 			log.SetVerbose(GeneralConfig.Verbose) | ||||
|  | ||||
| 			path, _ := os.Getwd() | ||||
| 			fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} | ||||
| 			log.RegisterHook(fatalHook) | ||||
|  | ||||
| 			err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) | ||||
| 			if err != nil { | ||||
| 				log.SetErrorCategory(log.ErrorConfiguration) | ||||
| 				return err | ||||
| 			} | ||||
| 			log.RegisterSecret(stepConfig.JenkinsURL) | ||||
| 			log.RegisterSecret(stepConfig.JenkinsUsername) | ||||
| 			log.RegisterSecret(stepConfig.JenkinsToken) | ||||
|  | ||||
| 			if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { | ||||
| 				sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) | ||||
| 				log.RegisterHook(&sentryHook) | ||||
| 			} | ||||
|  | ||||
| 			return nil | ||||
| 		}, | ||||
| 		Run: func(_ *cobra.Command, _ []string) { | ||||
| 			telemetryData := telemetry.CustomData{} | ||||
| 			telemetryData.ErrorCode = "1" | ||||
| 			handler := func() { | ||||
| 				config.RemoveVaultSecretFiles() | ||||
| 				telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) | ||||
| 				telemetryData.ErrorCategory = log.GetErrorCategory().String() | ||||
| 				telemetry.Send(&telemetryData) | ||||
| 			} | ||||
| 			log.DeferExitHandler(handler) | ||||
| 			defer handler() | ||||
| 			telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) | ||||
| 			vaultRotateSecretId(stepConfig, &telemetryData) | ||||
| 			telemetryData.ErrorCode = "0" | ||||
| 			log.Entry().Info("SUCCESS") | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	addVaultRotateSecretIdFlags(createVaultRotateSecretIdCmd, &stepConfig) | ||||
| 	return createVaultRotateSecretIdCmd | ||||
| } | ||||
|  | ||||
| func addVaultRotateSecretIdFlags(cmd *cobra.Command, stepConfig *vaultRotateSecretIdOptions) { | ||||
| 	cmd.Flags().StringVar(&stepConfig.SecretStore, "secretStore", `jenkins`, "The store to which the secret should be written back to") | ||||
| 	cmd.Flags().StringVar(&stepConfig.JenkinsURL, "jenkinsUrl", os.Getenv("PIPER_jenkinsUrl"), "The jenkins url") | ||||
| 	cmd.Flags().StringVar(&stepConfig.JenkinsCredentialDomain, "jenkinsCredentialDomain", `_`, "The jenkins credential domain which should be used") | ||||
| 	cmd.Flags().StringVar(&stepConfig.JenkinsUsername, "jenkinsUsername", os.Getenv("PIPER_jenkinsUsername"), "The jenkins username") | ||||
| 	cmd.Flags().StringVar(&stepConfig.JenkinsToken, "jenkinsToken", os.Getenv("PIPER_jenkinsToken"), "The jenkins token") | ||||
| 	cmd.Flags().StringVar(&stepConfig.VaultAppRoleSecretTokenCredentialsID, "vaultAppRoleSecretTokenCredentialsId", os.Getenv("PIPER_vaultAppRoleSecretTokenCredentialsId"), "The Jenkins credential ID for the Vault AppRole Secret ID credential") | ||||
| 	cmd.Flags().StringVar(&stepConfig.VaultServerURL, "vaultServerUrl", os.Getenv("PIPER_vaultServerUrl"), "The URL for the Vault server to use") | ||||
| 	cmd.Flags().StringVar(&stepConfig.VaultNamespace, "vaultNamespace", os.Getenv("PIPER_vaultNamespace"), "The vault namespace that should be used (optional)") | ||||
| 	cmd.Flags().IntVar(&stepConfig.DaysBeforeExpiry, "daysBeforeExpiry", 15, "The amount of days before expiry until the secret ID gets rotated") | ||||
|  | ||||
| 	cmd.MarkFlagRequired("vaultAppRoleSecretTokenCredentialsId") | ||||
| 	cmd.MarkFlagRequired("vaultServerUrl") | ||||
| } | ||||
|  | ||||
| // retrieve step metadata | ||||
| func vaultRotateSecretIdMetadata() config.StepData { | ||||
| 	var theMetaData = config.StepData{ | ||||
| 		Metadata: config.StepMetadata{ | ||||
| 			Name:    "vaultRotateSecretId", | ||||
| 			Aliases: []config.Alias{}, | ||||
| 		}, | ||||
| 		Spec: config.StepSpec{ | ||||
| 			Inputs: config.StepInputs{ | ||||
| 				Parameters: []config.StepParameters{ | ||||
| 					{ | ||||
| 						Name:        "secretStore", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "jenkinsUrl", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "", | ||||
| 								Paths: []string{"$(vaultPath)/jenkins", "$(vaultBasePath)/$(vaultPipelineName)/jenkins", "$(vaultBasePath)/GROUP-SECRETS/jenkins"}, | ||||
| 								Type:  "vaultSecret", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{{Name: "url"}}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "jenkinsCredentialDomain", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "jenkinsUsername", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "", | ||||
| 								Paths: []string{"$(vaultPath)/jenkins", "$(vaultBasePath)/$(vaultPipelineName)/jenkins", "$(vaultBasePath)/GROUP-SECRETS/jenkins"}, | ||||
| 								Type:  "vaultSecret", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{{Name: "userId"}}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name: "jenkinsToken", | ||||
| 						ResourceRef: []config.ResourceReference{ | ||||
| 							{ | ||||
| 								Name:  "", | ||||
| 								Paths: []string{"$(vaultPath)/jenkins", "$(vaultBasePath)/$(vaultPipelineName)/jenkins", "$(vaultBasePath)/GROUP-SECRETS/jenkins"}, | ||||
| 								Type:  "vaultSecret", | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scope:     []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:      "string", | ||||
| 						Mandatory: false, | ||||
| 						Aliases:   []config.Alias{{Name: "token"}}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "vaultAppRoleSecretTokenCredentialsId", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "vaultServerUrl", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   true, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "vaultNamespace", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "string", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 					{ | ||||
| 						Name:        "daysBeforeExpiry", | ||||
| 						ResourceRef: []config.ResourceReference{}, | ||||
| 						Scope:       []string{"PARAMETERS", "STAGES", "STEPS"}, | ||||
| 						Type:        "int", | ||||
| 						Mandatory:   false, | ||||
| 						Aliases:     []config.Alias{}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	return theMetaData | ||||
| } | ||||
							
								
								
									
										16
									
								
								cmd/vaultRotateSecretId_generated_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								cmd/vaultRotateSecretId_generated_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestVaultRotateSecretIdCommand(t *testing.T) { | ||||
|  | ||||
| 	testCmd := VaultRotateSecretIdCommand() | ||||
|  | ||||
| 	// only high level testing performed - details are tested in step generation procedure | ||||
| 	assert.Equal(t, "vaultRotateSecretId", testCmd.Use, "command name incorrect") | ||||
|  | ||||
| } | ||||
							
								
								
									
										49
									
								
								cmd/vaultRotateSecretId_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								cmd/vaultRotateSecretId_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type mockVaultRotateSecretIDUtilsBundle struct { | ||||
| 	t                *testing.T | ||||
| 	newSecret        string | ||||
| 	ttl              time.Duration | ||||
| 	config           *vaultRotateSecretIdOptions | ||||
| 	updateFuncCalled bool | ||||
| } | ||||
|  | ||||
| func TestRunVaultRotateSecretId(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	mock := &mockVaultRotateSecretIDUtilsBundle{t, "test-secret", time.Hour, getTestConfig(), false} | ||||
| 	runVaultRotateSecretID(mock) | ||||
| 	assert.True(t, mock.updateFuncCalled) | ||||
|  | ||||
| } | ||||
|  | ||||
| func (v *mockVaultRotateSecretIDUtilsBundle) GenerateNewAppRoleSecret(secretID string, roleName string) (string, error) { | ||||
| 	return v.newSecret, nil | ||||
| } | ||||
|  | ||||
| func (v *mockVaultRotateSecretIDUtilsBundle) GetAppRoleSecretIDTtl(secretID, roleName string) (time.Duration, error) { | ||||
| 	return v.ttl, nil | ||||
| } | ||||
| func (v *mockVaultRotateSecretIDUtilsBundle) GetAppRoleName() (string, error) { | ||||
| 	return "test", nil | ||||
| } | ||||
| func (v *mockVaultRotateSecretIDUtilsBundle) UpdateSecretInStore(config *vaultRotateSecretIdOptions, secretID string) error { | ||||
| 	v.updateFuncCalled = true | ||||
| 	assert.Equal(v.t, v.newSecret, secretID) | ||||
| 	return nil | ||||
| } | ||||
| func (v *mockVaultRotateSecretIDUtilsBundle) GetConfig() *vaultRotateSecretIdOptions { | ||||
| 	return v.config | ||||
| } | ||||
|  | ||||
| func getTestConfig() *vaultRotateSecretIdOptions { | ||||
| 	return &vaultRotateSecretIdOptions{ | ||||
| 		DaysBeforeExpiry: 5, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								documentation/docs/steps/vaultRotateSecretId.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								documentation/docs/steps/vaultRotateSecretId.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| # ${docGenStepName} | ||||
|  | ||||
| ## ${docGenDescription} | ||||
|  | ||||
| ## Prerequisites | ||||
|  | ||||
| ## ${docGenParameters} | ||||
|  | ||||
| ## ${docGenConfiguration} | ||||
|  | ||||
| ## ${docJenkinsPluginDependencies} | ||||
|  | ||||
| ## Exceptions | ||||
|  | ||||
| none | ||||
|  | ||||
| ## Examples | ||||
| @@ -139,6 +139,7 @@ nav: | ||||
|         - transportRequestRelease:  steps/transportRequestRelease.md | ||||
|         - transportRequestUploadFile: steps/transportRequestUploadFile.md | ||||
|         - uiVeri5ExecuteTests: steps/uiVeri5ExecuteTests.md | ||||
|         - vaultRotateSecretId: steps/vaultRotateSecretId.md | ||||
|         - whitesourceExecuteScan: steps/whitesourceExecuteScan.md | ||||
|         - writeTemporaryCredentials: steps/writeTemporaryCredentials.md | ||||
|         - xsDeploy: steps/xsDeploy.md | ||||
|   | ||||
| @@ -5,6 +5,7 @@ package main | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| @@ -42,7 +43,7 @@ func TestGetVaultSecret(t *testing.T) { | ||||
| 	assert.NoError(t, err) | ||||
| 	port, err := vaultContainer.MappedPort(ctx, "8200") | ||||
| 	host := fmt.Sprintf("http://%s:%s", ip, port.Port()) | ||||
| 	config := &api.Config{Address: host} | ||||
| 	config := &vault.Config{Config: &api.Config{Address: host}} | ||||
| 	// setup vault for testing | ||||
| 	secretData := SecretData{ | ||||
| 		"key1": "value1", | ||||
| @@ -50,7 +51,7 @@ func TestGetVaultSecret(t *testing.T) { | ||||
| 	} | ||||
| 	setupVault(t, config, testToken, secretData) | ||||
|  | ||||
| 	client, err := vault.NewClient(config, testToken, "") | ||||
| 	client, err := vault.NewClient(config, testToken) | ||||
| 	assert.NoError(t, err) | ||||
| 	secret, err := client.GetKvSecret("secret/test") | ||||
| 	assert.NoError(t, err) | ||||
| @@ -64,10 +65,12 @@ func TestGetVaultSecret(t *testing.T) { | ||||
|  | ||||
| } | ||||
|  | ||||
| func TestVaultAppRoleLogin(t *testing.T) { | ||||
| func TestVaultAppRole(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	ctx := context.Background() | ||||
| 	const testToken = "vault-token" | ||||
| 	const appRolePath = "auth/approle/role/test" | ||||
| 	const appRoleName = "test" | ||||
|  | ||||
| 	req := testcontainers.GenericContainerRequest{ | ||||
| 		ContainerRequest: testcontainers.ContainerRequest{ | ||||
| @@ -88,31 +91,88 @@ func TestVaultAppRoleLogin(t *testing.T) { | ||||
| 	assert.NoError(t, err) | ||||
| 	port, err := vaultContainer.MappedPort(ctx, "8200") | ||||
| 	host := fmt.Sprintf("http://%s:%s", ip, port.Port()) | ||||
| 	config := &api.Config{Address: host} | ||||
| 	config := &vault.Config{Config: &api.Config{Address: host}} | ||||
|  | ||||
| 	roleID, secretID := setupVaultAppRole(t, config, testToken) | ||||
| 	client, err := vault.NewClientWithAppRole(config, roleID, secretID, "") | ||||
| 	assert.NoError(t, err) | ||||
| 	_, err = client.GetSecret("auth/token/lookup-self") | ||||
| 	assert.NoError(t, err) | ||||
| 	secretIDMetadata := map[string]interface{}{ | ||||
| 		"field1": "value1", | ||||
| 	} | ||||
|  | ||||
| 	roleID, secretID := setupVaultAppRole(t, config, testToken, appRolePath, secretIDMetadata) | ||||
|  | ||||
| 	t.Run("Test Vault AppRole login", func(t *testing.T) { | ||||
| 		client, err := vault.NewClientWithAppRole(config, roleID, secretID) | ||||
| 		assert.NoError(t, err) | ||||
| 		secret, err := client.GetSecret("auth/token/lookup-self") | ||||
| 		meta := secret.Data["meta"].(SecretData) | ||||
| 		assert.Equal(t, meta["field1"], "value1") | ||||
| 		assert.Equal(t, meta["role_name"], "test") | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test Vault AppRoleTTL Fetch", func(t *testing.T) { | ||||
| 		client, err := vault.NewClient(config, testToken) | ||||
| 		assert.NoError(t, err) | ||||
| 		ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, time.Duration(90*24*time.Hour), ttl.Round(time.Hour)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test Vault AppRole Rotation", func(t *testing.T) { | ||||
| 		client, err := vault.NewClient(config, testToken) | ||||
| 		assert.NoError(t, err) | ||||
| 		newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotEmpty(t, newSecretID) | ||||
| 		assert.NotEqual(t, secretID, newSecretID) | ||||
|  | ||||
| 		// verify metadata is not broken | ||||
| 		client, err = vault.NewClientWithAppRole(config, roleID, newSecretID) | ||||
| 		assert.NoError(t, err) | ||||
| 		secret, err := client.GetSecret("auth/token/lookup-self") | ||||
| 		meta := secret.Data["meta"].(SecretData) | ||||
| 		assert.Equal(t, meta["field1"], "value1") | ||||
| 		assert.Equal(t, meta["role_name"], "test") | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test Fetching RoleName from vault", func(t *testing.T) { | ||||
| 		client, err := vault.NewClientWithAppRole(config, roleID, secretID) | ||||
| 		assert.NoError(t, err) | ||||
| 		fetchedRoleName, err := client.GetAppRoleName() | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, appRoleName, fetchedRoleName) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func setupVaultAppRole(t *testing.T, config *api.Config, token string) (string, string) { | ||||
| func setupVaultAppRole(t *testing.T, config *vault.Config, token, appRolePath string, metadata map[string]interface{}) (string, string) { | ||||
| 	t.Helper() | ||||
| 	client, err := api.NewClient(config) | ||||
| 	client, err := api.NewClient(config.Config) | ||||
| 	assert.NoError(t, err) | ||||
| 	client.SetToken(token) | ||||
| 	lClient := client.Logical() | ||||
|  | ||||
| 	_, err = lClient.Write("sys/auth/approle", SecretData{ | ||||
| 		"type": "approle", | ||||
| 		"config": map[string]interface{}{ | ||||
| 			"default_lease_ttl": "7776000s", | ||||
| 			"max_lease_ttl":     "7776000s", | ||||
| 		}, | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	_, err = lClient.Write("auth/approle/role/test", SecretData{}) | ||||
| 	_, err = lClient.Write("auth/approle/role/test", SecretData{ | ||||
| 		"secret_id_ttl": 7776000, | ||||
| 	}) | ||||
|  | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	res, err := lClient.Write("auth/approle/role/test/secret-id", SecretData{}) | ||||
| 	metadataJson, err := json.Marshal(metadata) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	res, err := lClient.Write("auth/approle/role/test/secret-id", SecretData{ | ||||
| 		"metadata": string(metadataJson), | ||||
| 	}) | ||||
|  | ||||
| 	assert.NoError(t, err) | ||||
| 	secretID := res.Data["secret_id"] | ||||
|  | ||||
| @@ -123,9 +183,9 @@ func setupVaultAppRole(t *testing.T, config *api.Config, token string) (string, | ||||
| 	return roleID.(string), secretID.(string) | ||||
| } | ||||
|  | ||||
| func setupVault(t *testing.T, config *api.Config, token string, secret SecretData) { | ||||
| func setupVault(t *testing.T, config *vault.Config, token string, secret SecretData) { | ||||
| 	t.Helper() | ||||
| 	client, err := api.NewClient(config) | ||||
| 	client, err := api.NewClient(config.Config) | ||||
| 	assert.NoError(t, err) | ||||
| 	client.SetToken(token) | ||||
|  | ||||
|   | ||||
| @@ -227,6 +227,12 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri | ||||
| 		stepConfig.mixIn(flagValues, filters.Parameters) | ||||
| 	} | ||||
|  | ||||
| 	if verbose, ok := stepConfig.Config["verbose"].(bool); ok && verbose { | ||||
| 		log.SetVerbose(verbose) | ||||
| 	} else if !ok { | ||||
| 		log.Entry().Warnf("invalid value for parameter verbose: '%v'", stepConfig.Config["verbose"]) | ||||
| 	} | ||||
|  | ||||
| 	stepConfig.mixIn(c.General, vaultFilter) | ||||
| 	// fetch secrets from vault | ||||
| 	vaultClient, err := getVaultClientFromConfig(stepConfig, c.vaultCredentials) | ||||
| @@ -243,9 +249,9 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri | ||||
| 			cp := p.Conditions[0].Params[0] | ||||
| 			dependentValue := stepConfig.Config[cp.Name] | ||||
| 			if cmp.Equal(dependentValue, cp.Value) && stepConfig.Config[p.Name] == nil { | ||||
| 				subMapValue := stepConfig.Config[dependentValue.(string)].(map[string]interface{})[p.Name] | ||||
| 				if subMapValue != nil { | ||||
| 					stepConfig.Config[p.Name] = subMapValue | ||||
| 				subMap, ok := stepConfig.Config[dependentValue.(string)].(map[string]interface{}) | ||||
| 				if ok && subMap[p.Name] != nil { | ||||
| 					stepConfig.Config[p.Name] = subMap[p.Name] | ||||
| 				} else { | ||||
| 					stepConfig.Config[p.Name] = p.Default | ||||
| 				} | ||||
|   | ||||
| @@ -50,7 +50,7 @@ func getVaultClientFromConfig(config StepConfig, creds VaultCredentials) (vaultC | ||||
| 		log.Entry().Debugf("Using vault namespace %s", namespace) | ||||
| 	} | ||||
|  | ||||
| 	client, err := vault.NewClientWithAppRole(&api.Config{Address: address}, creds.AppRoleID, creds.AppRoleSecretID, namespace) | ||||
| 	client, err := vault.NewClientWithAppRole(&vault.Config{Config: &api.Config{Address: address}, Namespace: namespace}, creds.AppRoleID, creds.AppRoleSecretID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| @@ -148,9 +148,10 @@ func lookupPath(client vaultClient, path string, param *StepParameters) *string | ||||
| 		log.RegisterSecret(field) | ||||
| 		return &field | ||||
| 	} | ||||
|  | ||||
| 	log.Entry().Debugf("Secret did not contain a field name '%s'", param.Name) | ||||
| 	// try parameter aliases | ||||
| 	for _, alias := range param.Aliases { | ||||
| 		log.Entry().Debugf("Trying alias field name '%s'", alias.Name) | ||||
| 		field := secret[alias.Name] | ||||
| 		if field != "" { | ||||
| 			log.RegisterSecret(field) | ||||
|   | ||||
							
								
								
									
										51
									
								
								pkg/jenkins/credential.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								pkg/jenkins/credential.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| package jenkins | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
|  | ||||
| 	"github.com/bndr/gojenkins" | ||||
| ) | ||||
|  | ||||
| // StringCredentials store only secret text | ||||
| type StringCredentials = gojenkins.StringCredentials | ||||
|  | ||||
| //UsernameCredentials struct representing credential for storing username-password pair | ||||
| type UsernameCredentials = gojenkins.UsernameCredentials | ||||
|  | ||||
| // SSHCredentials store credentials for ssh keys. | ||||
| type SSHCredentials = gojenkins.SSHCredentials | ||||
|  | ||||
| // DockerServerCredentials store credentials for docker keys. | ||||
| type DockerServerCredentials = gojenkins.DockerServerCredentials | ||||
|  | ||||
| // CredentialsManager is utility to control credential plugin | ||||
| type CredentialsManager interface { | ||||
| 	Update(string, string, interface{}) error | ||||
| } | ||||
|  | ||||
| // NewCredentialsManager returns a new CredentialManager | ||||
| func NewCredentialsManager(jenkins *gojenkins.Jenkins) CredentialsManager { | ||||
| 	return gojenkins.CredentialsManager{J: jenkins} | ||||
| } | ||||
|  | ||||
| // UpdateCredential overwrites an existing credential | ||||
| func UpdateCredential(credentialsManager CredentialsManager, domain string, credential interface{}) error { | ||||
| 	credValue := reflect.ValueOf(credential) | ||||
| 	if credValue.Kind() != reflect.Struct { | ||||
| 		return fmt.Errorf("'credential' parameter is supposed to be a Credentials struct not '%s'", credValue.Type()) | ||||
| 	} | ||||
|  | ||||
| 	idField := credValue.FieldByName("ID") | ||||
| 	if !idField.IsValid() || idField.Kind() != reflect.String { | ||||
| 		return fmt.Errorf("'credential' parameter is supposed to be a Credentials struct not '%s'", credValue.Type()) | ||||
| 	} | ||||
|  | ||||
| 	secretID := idField.String() | ||||
| 	if secretID == "" { | ||||
| 		return errors.New("Secret ID should not be empty") | ||||
| 	} | ||||
|  | ||||
| 	return credentialsManager.Update(domain, secretID, credential) | ||||
| } | ||||
							
								
								
									
										59
									
								
								pkg/jenkins/credential_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								pkg/jenkins/credential_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| package jenkins | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/jenkins/mocks" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/mock" | ||||
| ) | ||||
|  | ||||
| func TestUpdateCredential(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	const ID = "testID" | ||||
| 	const testSecret = "testSecret" | ||||
| 	const domain = "_" | ||||
|  | ||||
| 	t.Run("That secret is updated", func(t *testing.T) { | ||||
| 		credManagerMock := mocks.CredentialsManager{} | ||||
| 		testCredential := StringCredentials{ID: ID, Secret: testSecret} | ||||
|  | ||||
| 		credManagerMock.On("Update", domain, ID, mock.Anything).Return(nil) | ||||
| 		err := UpdateCredential(&credManagerMock, domain, testCredential) | ||||
| 		credManagerMock.AssertCalled(t, "Update", domain, ID, testCredential) | ||||
|  | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test that wrong credential type fails ", func(t *testing.T) { | ||||
| 		credManagerMock := mocks.CredentialsManager{} | ||||
|  | ||||
| 		credManagerMock.On("Update", domain, ID, mock.Anything).Return(nil) | ||||
| 		err := UpdateCredential(&credManagerMock, domain, 5) | ||||
| 		credManagerMock.AssertNotCalled(t, "Update", domain, ID, mock.Anything) | ||||
| 		assert.EqualError(t, err, "'credential' parameter is supposed to be a Credentials struct not 'int'") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test that wrong credential type fails ", func(t *testing.T) { | ||||
| 		credManagerMock := mocks.CredentialsManager{} | ||||
| 		testCredential := struct{ Secret string }{ | ||||
| 			Secret: "Test", | ||||
| 		} | ||||
|  | ||||
| 		credManagerMock.On("Update", domain, ID, mock.Anything).Return(nil) | ||||
| 		err := UpdateCredential(&credManagerMock, domain, testCredential) | ||||
| 		credManagerMock.AssertNotCalled(t, "Update", domain, ID, mock.Anything) | ||||
| 		assert.EqualError(t, err, "'credential' parameter is supposed to be a Credentials struct not 'struct { Secret string }'") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test that empty secret id fails ", func(t *testing.T) { | ||||
| 		credManagerMock := mocks.CredentialsManager{} | ||||
| 		testCredential := StringCredentials{ID: "", Secret: testSecret} | ||||
|  | ||||
| 		credManagerMock.On("Update", domain, ID, mock.Anything).Return(nil) | ||||
| 		err := UpdateCredential(&credManagerMock, domain, testCredential) | ||||
| 		credManagerMock.AssertNotCalled(t, "Update", domain, ID, mock.Anything) | ||||
| 		assert.EqualError(t, err, "Secret ID should not be empty") | ||||
| 	}) | ||||
|  | ||||
| } | ||||
							
								
								
									
										24
									
								
								pkg/jenkins/mocks/CredentialsManager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								pkg/jenkins/mocks/CredentialsManager.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // Code generated by mockery v2.3.0. DO NOT EDIT. | ||||
|  | ||||
| package mocks | ||||
|  | ||||
| import mock "github.com/stretchr/testify/mock" | ||||
|  | ||||
| // CredentialsManager is an autogenerated mock type for the CredentialsManager type | ||||
| type CredentialsManager struct { | ||||
| 	mock.Mock | ||||
| } | ||||
|  | ||||
| // Update provides a mock function with given fields: _a0, _a1, _a2 | ||||
| func (_m *CredentialsManager) Update(_a0 string, _a1 string, _a2 interface{}) error { | ||||
| 	ret := _m.Called(_a0, _a1, _a2) | ||||
|  | ||||
| 	var r0 error | ||||
| 	if rf, ok := ret.Get(0).(func(string, string, interface{}) error); ok { | ||||
| 		r0 = rf(_a0, _a1, _a2) | ||||
| 	} else { | ||||
| 		r0 = ret.Error(0) | ||||
| 	} | ||||
|  | ||||
| 	return r0 | ||||
| } | ||||
| @@ -1,10 +1,12 @@ | ||||
| package vault | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/SAP/jenkins-library/pkg/log" | ||||
| 	"github.com/hashicorp/vault/api" | ||||
| @@ -13,48 +15,61 @@ import ( | ||||
| // Client handles communication with Vault | ||||
| type Client struct { | ||||
| 	lClient logicalClient | ||||
| 	config  *Config | ||||
| } | ||||
|  | ||||
| // Config contains the vault client configuration | ||||
| type Config struct { | ||||
| 	*api.Config | ||||
| 	AppRoleMountPoint string | ||||
| 	Namespace         string | ||||
| } | ||||
|  | ||||
| // logicalClient interface for mocking | ||||
| type logicalClient interface { | ||||
| 	Read(string) (*api.Secret, error) | ||||
| 	Write(string, map[string]interface{}) (*api.Secret, error) | ||||
| } | ||||
|  | ||||
| // NewClient instantiates a Client and sets the specified token | ||||
| func NewClient(config *api.Config, token, namespace string) (Client, error) { | ||||
| func NewClient(config *Config, token string) (Client, error) { | ||||
| 	if config == nil { | ||||
| 		config = api.DefaultConfig() | ||||
| 		config = &Config{Config: api.DefaultConfig()} | ||||
| 	} | ||||
| 	client, err := api.NewClient(config) | ||||
| 	client, err := api.NewClient(config.Config) | ||||
| 	if err != nil { | ||||
| 		return Client{}, err | ||||
| 	} | ||||
|  | ||||
| 	if namespace != "" { | ||||
| 		client.SetNamespace(namespace) | ||||
| 	if config.Namespace != "" { | ||||
| 		client.SetNamespace(config.Namespace) | ||||
| 	} | ||||
|  | ||||
| 	client.SetToken(token) | ||||
| 	return Client{client.Logical()}, nil | ||||
| 	return Client{client.Logical(), config}, nil | ||||
| } | ||||
|  | ||||
| // NewClientWithAppRole instantiates a new client and obtains a token via the AppRole auth method | ||||
| func NewClientWithAppRole(config *api.Config, roleID, secretID, namespace string) (Client, error) { | ||||
| func NewClientWithAppRole(config *Config, roleID, secretID string) (Client, error) { | ||||
| 	if config == nil { | ||||
| 		config = api.DefaultConfig() | ||||
| 		config = &Config{Config: api.DefaultConfig()} | ||||
| 	} | ||||
|  | ||||
| 	client, err := api.NewClient(config) | ||||
| 	if config.AppRoleMountPoint == "" { | ||||
| 		config.AppRoleMountPoint = "auth/approle" | ||||
| 	} | ||||
|  | ||||
| 	client, err := api.NewClient(config.Config) | ||||
| 	if err != nil { | ||||
| 		return Client{}, err | ||||
| 	} | ||||
|  | ||||
| 	if namespace != "" { | ||||
| 		client.SetNamespace(namespace) | ||||
| 	if config.Namespace != "" { | ||||
| 		client.SetNamespace(config.Namespace) | ||||
| 	} | ||||
|  | ||||
| 	log.Entry().Debug("Using approle login") | ||||
| 	result, err := client.Logical().Write("auth/approle/login", map[string]interface{}{ | ||||
| 	result, err := client.Logical().Write(path.Join(config.AppRoleMountPoint, "/login"), map[string]interface{}{ | ||||
| 		"role_id":   roleID, | ||||
| 		"secret_id": secretID, | ||||
| 	}) | ||||
| @@ -68,8 +83,8 @@ func NewClientWithAppRole(config *api.Config, roleID, secretID, namespace string | ||||
| 		return Client{}, fmt.Errorf("Could not obtain token from approle with role_id %s", roleID) | ||||
| 	} | ||||
|  | ||||
| 	log.Entry().Debugf("Login to vault %s in namespace %s successfull", config.Address, namespace) | ||||
| 	return NewClient(config, authInfo.ClientToken, namespace) | ||||
| 	log.Entry().Debugf("Login to vault %s in namespace %s successfull", config.Address, config.Namespace) | ||||
| 	return NewClient(config, authInfo.ClientToken) | ||||
| } | ||||
|  | ||||
| // GetSecret uses the given path to fetch a secret from vault | ||||
| @@ -124,14 +139,145 @@ func (v Client) GetKvSecret(path string) (map[string]string, error) { | ||||
| 	secretData := make(map[string]string, len(data)) | ||||
| 	for k, v := range data { | ||||
| 		valueStr, ok := v.(string) | ||||
| 		if !ok { | ||||
| 			return nil, fmt.Errorf("Expected secret value to be a string but got %T instead", v) | ||||
| 		if ok { | ||||
| 			secretData[k] = valueStr | ||||
| 		} | ||||
| 		secretData[k] = valueStr | ||||
| 	} | ||||
| 	return secretData, nil | ||||
| } | ||||
|  | ||||
| // GenerateNewAppRoleSecret creates a new secret-id | ||||
| func (v *Client) GenerateNewAppRoleSecret(secretID, appRoleName string) (string, error) { | ||||
| 	appRolePath := v.getAppRolePath(appRoleName) | ||||
| 	secretIDData, err := v.lookupSecretID(secretID, appRolePath) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	reqPath := sanitizePath(path.Join(appRolePath, "/secret-id")) | ||||
|  | ||||
| 	// we preserve metadata which was attached to the secret-id | ||||
| 	json, err := json.Marshal(secretIDData["metadata"]) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	secret, err := v.lClient.Write(reqPath, map[string]interface{}{ | ||||
| 		"metadata": string(json), | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if secret == nil || secret.Data == nil { | ||||
| 		return "", fmt.Errorf("Could not generate new approle secret-id for approle path %s", reqPath) | ||||
| 	} | ||||
|  | ||||
| 	secretIDRaw, ok := secret.Data["secret_id"] | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("Vault response for path %s did not contain a new secret-id", reqPath) | ||||
| 	} | ||||
|  | ||||
| 	newSecretID, ok := secretIDRaw.(string) | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("New secret-id from approle path %s has an unexpected type %T expected 'string'", reqPath, secretIDRaw) | ||||
| 	} | ||||
|  | ||||
| 	return newSecretID, nil | ||||
| } | ||||
|  | ||||
| // GetAppRoleSecretIDTtl returns the remaining time until the given secret-id expires | ||||
| func (v *Client) GetAppRoleSecretIDTtl(secretID, roleName string) (time.Duration, error) { | ||||
| 	appRolePath := v.getAppRolePath(roleName) | ||||
| 	data, err := v.lookupSecretID(secretID, appRolePath) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	if data == nil || data["expiration_time"] == nil { | ||||
| 		return 0, fmt.Errorf("Could not load secret-id information from path %s", appRolePath) | ||||
| 	} | ||||
|  | ||||
| 	expiration, ok := data["expiration_time"].(string) | ||||
| 	if !ok || expiration == "" { | ||||
| 		return 0, fmt.Errorf("Could not handle get expiration time for secret-id from path %s", appRolePath) | ||||
| 	} | ||||
|  | ||||
| 	expirationDate, err := time.Parse(time.RFC3339, expiration) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	ttl := expirationDate.Sub(time.Now()) | ||||
| 	if ttl < 0 { | ||||
| 		return 0, nil | ||||
| 	} | ||||
|  | ||||
| 	return ttl, nil | ||||
| } | ||||
|  | ||||
| // GetAppRoleName returns the AppRole role name which was used to authenticate. | ||||
| // Returns "" when AppRole authentication wasn't used | ||||
| func (v *Client) GetAppRoleName() (string, error) { | ||||
| 	const lookupPath = "auth/token/lookup-self" | ||||
| 	secret, err := v.GetSecret(lookupPath) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if secret.Data == nil { | ||||
| 		return "", fmt.Errorf("Could not lookup token information: %s", lookupPath) | ||||
| 	} | ||||
|  | ||||
| 	meta, ok := secret.Data["meta"] | ||||
|  | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("Token info did not contain metadata %s", lookupPath) | ||||
| 	} | ||||
|  | ||||
| 	metaMap, ok := meta.(map[string]interface{}) | ||||
|  | ||||
| 	if !ok { | ||||
| 		return "", fmt.Errorf("Token info field 'meta' is not a map: %s", lookupPath) | ||||
| 	} | ||||
|  | ||||
| 	roleName := metaMap["role_name"] | ||||
|  | ||||
| 	if roleName == nil { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	roleNameStr, ok := roleName.(string) | ||||
| 	if !ok { | ||||
| 		// when approle authentication is not used vault admins can use the role_name field with other type | ||||
| 		// so no error in this case | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	return roleNameStr, nil | ||||
| } | ||||
|  | ||||
| // SetAppRoleMountPoint sets the path under which the approle auth backend is mounted | ||||
| func (v *Client) SetAppRoleMountPoint(appRoleMountpoint string) { | ||||
| 	v.config.AppRoleMountPoint = appRoleMountpoint | ||||
| } | ||||
|  | ||||
| func (v *Client) getAppRolePath(roleName string) string { | ||||
| 	appRoleMountPoint := v.config.AppRoleMountPoint | ||||
| 	if appRoleMountPoint == "" { | ||||
| 		appRoleMountPoint = "auth/approle" | ||||
| 	} | ||||
| 	return path.Join(appRoleMountPoint, "role", roleName) | ||||
| } | ||||
|  | ||||
| func sanitizePath(path string) string { | ||||
| 	path = strings.TrimSpace(path) | ||||
| 	path = strings.TrimPrefix(path, "/") | ||||
| 	path = strings.TrimSuffix(path, "/") | ||||
| 	return path | ||||
| } | ||||
|  | ||||
| func addPrefixToKvPath(p, mountPath, apiPrefix string) string { | ||||
| 	switch { | ||||
| 	case p == mountPath, p == strings.TrimSuffix(mountPath, "/"): | ||||
| @@ -180,9 +326,14 @@ func (v *Client) getKvInfo(path string) (string, int, error) { | ||||
| 	return mountPath, vNumber, nil | ||||
| } | ||||
|  | ||||
| func sanitizePath(path string) string { | ||||
| 	path = strings.TrimSpace(path) | ||||
| 	path = strings.TrimPrefix(path, "/") | ||||
| 	path = strings.TrimSuffix(path, "/") | ||||
| 	return path | ||||
| func (v *Client) lookupSecretID(secretID, appRolePath string) (map[string]interface{}, error) { | ||||
| 	reqPath := sanitizePath(path.Join(appRolePath, "/secret-id/lookup")) | ||||
| 	secret, err := v.lClient.Write(reqPath, map[string]interface{}{ | ||||
| 		"secret_id": secretID, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return secret.Data, nil | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| package vault | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/mock" | ||||
|  | ||||
| @@ -22,7 +26,7 @@ func TestGetKV2Secret(t *testing.T) { | ||||
|  | ||||
| 	t.Run("Test missing secret", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		setupMockKvV2(vaultMock) | ||||
| 		vaultMock.On("Read", "secret/data/notexist").Return(nil, nil) | ||||
| 		secret, err := client.GetKvSecret("secret/notexist") | ||||
| @@ -37,7 +41,7 @@ func TestGetKV2Secret(t *testing.T) { | ||||
| 		t.Run("Getting secret from KV engine (v2)", func(t *testing.T) { | ||||
| 			vaultMock := &mocks.VaultMock{} | ||||
| 			setupMockKvV2(vaultMock) | ||||
| 			client := Client{vaultMock} | ||||
| 			client := Client{vaultMock, &Config{}} | ||||
| 			vaultMock.On("Read", secretAPIPath).Return(kv2Secret(SecretData{"key1": "value1"}), nil) | ||||
| 			secret, err := client.GetKvSecret(secretName) | ||||
| 			assert.NoError(t, err, "Expect GetKvSecret to succeed") | ||||
| @@ -45,21 +49,20 @@ func TestGetKV2Secret(t *testing.T) { | ||||
|  | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("error is thrown when 'data' field can't be parsed", func(t *testing.T) { | ||||
| 		t.Run("field ignored when 'data' field can't be parsed", func(t *testing.T) { | ||||
| 			vaultMock := &mocks.VaultMock{} | ||||
| 			setupMockKvV2(vaultMock) | ||||
| 			client := Client{vaultMock} | ||||
| 			client := Client{vaultMock, &Config{}} | ||||
| 			vaultMock.On("Read", secretAPIPath).Return(kv2Secret(SecretData{"key1": "value1", "key2": 5}), nil) | ||||
| 			secret, err := client.GetKvSecret(secretName) | ||||
| 			assert.Error(t, err, "Excpected to fail since value is wrong data type") | ||||
| 			assert.Nil(t, secret) | ||||
|  | ||||
| 			assert.NoError(t, err) | ||||
| 			assert.Empty(t, secret["key2"]) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("error is thrown when data field is missing", func(t *testing.T) { | ||||
| 			vaultMock := &mocks.VaultMock{} | ||||
| 			setupMockKvV2(vaultMock) | ||||
| 			client := Client{vaultMock} | ||||
| 			client := Client{vaultMock, &Config{}} | ||||
| 			vaultMock.On("Read", secretAPIPath).Return(kv1Secret(SecretData{"key1": "value1"}), nil) | ||||
| 			secret, err := client.GetKvSecret(secretName) | ||||
| 			assert.Error(t, err, "Expected to fail since 'data' field is missing") | ||||
| @@ -70,13 +73,12 @@ func TestGetKV2Secret(t *testing.T) { | ||||
|  | ||||
| func TestGetKV1Secret(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	const secretName = "secret/test" | ||||
|  | ||||
| 	t.Run("Test missing secret", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		setupMockKvV1(vaultMock) | ||||
| 		client := Client{vaultMock} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
|  | ||||
| 		vaultMock.On("Read", mock.AnythingOfType("string")).Return(nil, nil) | ||||
| 		secret, err := client.GetKvSecret("secret/notexist") | ||||
| @@ -87,7 +89,7 @@ func TestGetKV1Secret(t *testing.T) { | ||||
| 	t.Run("Test parsing KV1 secrets", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		setupMockKvV1(vaultMock) | ||||
| 		client := Client{vaultMock} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
|  | ||||
| 		vaultMock.On("Read", secretName).Return(kv1Secret(SecretData{"key1": "value1"}), nil) | ||||
| 		secret, err := client.GetKvSecret(secretName) | ||||
| @@ -99,18 +101,216 @@ func TestGetKV1Secret(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		setupMockKvV1(vaultMock) | ||||
| 		vaultMock.On("Read", secretName).Return(kv1Secret(SecretData{"key1": 5}), nil) | ||||
| 		client := Client{vaultMock} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
|  | ||||
| 		secret, err := client.GetKvSecret(secretName) | ||||
| 		assert.Error(t, err) | ||||
| 		assert.Nil(t, secret) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Empty(t, secret["key1"]) | ||||
|  | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestSecretIDGeneration(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	const secretID = "secret-id" | ||||
| 	const appRoleName = "test" | ||||
| 	const appRolePath = "auth/approle/role/test" | ||||
|  | ||||
| 	t.Run("Test generating new secret-id", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		now := time.Now() | ||||
| 		expiry := now.Add(5 * time.Hour).Format(time.RFC3339) | ||||
| 		metadata := map[string]interface{}{ | ||||
| 			"field1": "value1", | ||||
| 		} | ||||
|  | ||||
| 		metadataJSON, err := json.Marshal(metadata) | ||||
| 		assert.NoError(t, err) | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{ | ||||
| 			"expiration_time": expiry, | ||||
| 			"metadata":        metadata, | ||||
| 		}), nil) | ||||
|  | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "/secret-id"), mapWith("metadata", string(metadataJSON))).Return(kv1Secret(SecretData{ | ||||
| 			"secret_id": "newSecretId", | ||||
| 		}), nil) | ||||
|  | ||||
| 		newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "newSecretId", newSecretID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test with no secret-id returned", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		now := time.Now() | ||||
| 		expiry := now.Add(5 * time.Hour).Format(time.RFC3339) | ||||
| 		metadata := map[string]interface{}{ | ||||
| 			"field1": "value1", | ||||
| 		} | ||||
|  | ||||
| 		metadataJSON, err := json.Marshal(metadata) | ||||
| 		assert.NoError(t, err) | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{ | ||||
| 			"expiration_time": expiry, | ||||
| 			"metadata":        metadata, | ||||
| 		}), nil) | ||||
|  | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "/secret-id"), mapWith("metadata", string(metadataJSON))).Return(kv1Secret(SecretData{}), nil) | ||||
|  | ||||
| 		newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName) | ||||
| 		assert.EqualError(t, err, fmt.Sprintf("Vault response for path %s did not contain a new secret-id", path.Join(appRolePath, "secret-id"))) | ||||
| 		assert.Equal(t, newSecretID, "") | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test with no new secret-id returned", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		now := time.Now() | ||||
| 		expiry := now.Add(5 * time.Hour).Format(time.RFC3339) | ||||
| 		metadata := map[string]interface{}{ | ||||
| 			"field1": "value1", | ||||
| 		} | ||||
|  | ||||
| 		metadataJSON, err := json.Marshal(metadata) | ||||
| 		assert.NoError(t, err) | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{ | ||||
| 			"expiration_time": expiry, | ||||
| 			"metadata":        metadata, | ||||
| 		}), nil) | ||||
|  | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "/secret-id"), mapWith("metadata", string(metadataJSON))).Return(kv1Secret(nil), nil) | ||||
|  | ||||
| 		newSecretID, err := client.GenerateNewAppRoleSecret(secretID, appRoleName) | ||||
| 		assert.EqualError(t, err, fmt.Sprintf("Could not generate new approle secret-id for approle path %s", path.Join(appRolePath, "secret-id"))) | ||||
| 		assert.Equal(t, newSecretID, "") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestSecretIDTtl(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	const secretID = "secret-id" | ||||
| 	const appRolePath = "auth/approle/role/test" | ||||
| 	const appRoleName = "test" | ||||
|  | ||||
| 	t.Run("Test fetching secreID TTL", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		now := time.Now() | ||||
| 		expiry := now.Add(5 * time.Hour).Format(time.RFC3339) | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{ | ||||
| 			"expiration_time": expiry, | ||||
| 		}), nil) | ||||
|  | ||||
| 		ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, 5*time.Hour, ttl.Round(time.Hour)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test with no expiration time", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{}), nil) | ||||
| 		ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName) | ||||
| 		assert.EqualError(t, err, fmt.Sprintf("Could not load secret-id information from path %s", appRolePath)) | ||||
| 		assert.Equal(t, time.Duration(0), ttl) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test with wrong date format", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{ | ||||
| 			"expiration_time": time.Now().String(), | ||||
| 		}), nil) | ||||
| 		ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName) | ||||
| 		assert.True(t, strings.HasPrefix(err.Error(), "parsing time")) | ||||
| 		assert.Equal(t, time.Duration(0), ttl) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test with expired secret-id", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		now := time.Now() | ||||
| 		expiry := now.Add(-5 * time.Hour).Format(time.RFC3339) | ||||
| 		vaultMock.On("Write", path.Join(appRolePath, "secret-id/lookup"), mapWith("secret_id", secretID)).Return(kv1Secret(SecretData{ | ||||
| 			"expiration_time": expiry, | ||||
| 		}), nil) | ||||
|  | ||||
| 		ttl, err := client.GetAppRoleSecretIDTtl(secretID, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, time.Duration(0), ttl) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGetAppRoleName(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 	const secretID = "secret-id" | ||||
|  | ||||
| 	t.Run("Test that correct role name is returned", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{ | ||||
| 			"meta": SecretData{ | ||||
| 				"role_name": "test", | ||||
| 			}, | ||||
| 		}), nil) | ||||
|  | ||||
| 		appRoleName, err := client.GetAppRoleName() | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "test", appRoleName) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test without secret data", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(nil), nil) | ||||
|  | ||||
| 		appRoleName, err := client.GetAppRoleName() | ||||
| 		assert.EqualError(t, err, "Could not lookup token information: auth/token/lookup-self") | ||||
| 		assert.Empty(t, appRoleName) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test without metadata data", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{}), nil) | ||||
|  | ||||
| 		appRoleName, err := client.GetAppRoleName() | ||||
| 		assert.EqualError(t, err, "Token info did not contain metadata auth/token/lookup-self") | ||||
| 		assert.Empty(t, appRoleName) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test without role name in metadata", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{ | ||||
| 			"meta": SecretData{}, | ||||
| 		}), nil) | ||||
|  | ||||
| 		appRoleName, err := client.GetAppRoleName() | ||||
| 		assert.Empty(t, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Test that different role_name types are ignored", func(t *testing.T) { | ||||
| 		vaultMock := &mocks.VaultMock{} | ||||
| 		client := Client{vaultMock, &Config{}} | ||||
| 		vaultMock.On("Read", "auth/token/lookup-self").Return(kv1Secret(SecretData{ | ||||
| 			"meta": SecretData{ | ||||
| 				"role_name": 5, | ||||
| 			}, | ||||
| 		}), nil) | ||||
|  | ||||
| 		appRoleName, err := client.GetAppRoleName() | ||||
| 		assert.Empty(t, appRoleName) | ||||
| 		assert.NoError(t, err) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestUnknownKvVersion(t *testing.T) { | ||||
| 	vaultMock := &mocks.VaultMock{} | ||||
| 	client := Client{vaultMock} | ||||
| 	client := Client{vaultMock, &Config{}} | ||||
|  | ||||
| 	vaultMock.On("Read", "sys/internal/ui/mounts/secret/secret").Return(&api.Secret{ | ||||
| 		Data: map[string]interface{}{ | ||||
| @@ -126,6 +326,15 @@ func TestUnknownKvVersion(t *testing.T) { | ||||
|  | ||||
| } | ||||
|  | ||||
| func TestSetAppRoleMountPont(t *testing.T) { | ||||
| 	client := Client{nil, &Config{}} | ||||
| 	const newMountpoint = "auth/test" | ||||
|  | ||||
| 	client.SetAppRoleMountPoint("auth/test") | ||||
|  | ||||
| 	assert.Equal(t, newMountpoint, client.config.AppRoleMountPoint) | ||||
| } | ||||
|  | ||||
| func setupMockKvV2(vaultMock *mocks.VaultMock) { | ||||
| 	vaultMock.On("Read", mock.MatchedBy(func(path string) bool { | ||||
| 		return strings.HasPrefix(path, sysLookupPath) | ||||
| @@ -174,3 +383,19 @@ func kv2Secret(data SecretData) *api.Secret { | ||||
| 		Data: SecretData{"data": data}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func mapWith(key, expectedValue string) interface{} { | ||||
| 	return mock.MatchedBy(func(arg map[string]interface{}) bool { | ||||
| 		valRaw, ok := arg[key] | ||||
| 		if !ok { | ||||
| 			return false | ||||
| 		} | ||||
|  | ||||
| 		val, ok := valRaw.(string) | ||||
| 		if !ok { | ||||
| 			return false | ||||
| 		} | ||||
|  | ||||
| 		return val == expectedValue | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| // +build !release | ||||
| // Code generated by mockery v2.0.3. DO NOT EDIT. | ||||
| // Code generated by mockery v2.3.0. DO NOT EDIT. | ||||
|  | ||||
| package mocks | ||||
|  | ||||
| @@ -35,3 +34,26 @@ func (_m *VaultMock) Read(_a0 string) (*api.Secret, error) { | ||||
|  | ||||
| 	return r0, r1 | ||||
| } | ||||
|  | ||||
| // Write provides a mock function with given fields: _a0, _a1 | ||||
| func (_m *VaultMock) Write(_a0 string, _a1 map[string]interface{}) (*api.Secret, error) { | ||||
| 	ret := _m.Called(_a0, _a1) | ||||
|  | ||||
| 	var r0 *api.Secret | ||||
| 	if rf, ok := ret.Get(0).(func(string, map[string]interface{}) *api.Secret); ok { | ||||
| 		r0 = rf(_a0, _a1) | ||||
| 	} else { | ||||
| 		if ret.Get(0) != nil { | ||||
| 			r0 = ret.Get(0).(*api.Secret) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var r1 error | ||||
| 	if rf, ok := ret.Get(1).(func(string, map[string]interface{}) error); ok { | ||||
| 		r1 = rf(_a0, _a1) | ||||
| 	} else { | ||||
| 		r1 = ret.Error(1) | ||||
| 	} | ||||
|  | ||||
| 	return r0, r1 | ||||
| } | ||||
|   | ||||
							
								
								
									
										107
									
								
								resources/metadata/vaultRotateSecretId.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								resources/metadata/vaultRotateSecretId.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| metadata: | ||||
|   name: vaultRotateSecretId | ||||
|   description: Rotate vault AppRole Secret ID | ||||
|   longDescription: This step takes the given Vault secret ID and checks whether it needs to be renewed and if so it will update the secret ID in the configured secret store. | ||||
| spec: | ||||
|   inputs: | ||||
|     params: | ||||
|       - name: secretStore | ||||
|         type: string | ||||
|         description: "The store to which the secret should be written back to" | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         default: "jenkins" | ||||
|         possibleValues: | ||||
|           - jenkins | ||||
|       - name: jenkinsUrl | ||||
|         type: string | ||||
|         description: "The jenkins url" | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         secret: true | ||||
|         resourceRef: | ||||
|           - type: vaultSecret | ||||
|             paths: | ||||
|               - $(vaultPath)/jenkins | ||||
|               - $(vaultBasePath)/$(vaultPipelineName)/jenkins | ||||
|               - $(vaultBasePath)/GROUP-SECRETS/jenkins | ||||
|         aliases: | ||||
|           - name: url | ||||
|       - name: jenkinsCredentialDomain | ||||
|         type: string | ||||
|         description: The jenkins credential domain which should be used | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         default: "_" | ||||
|       - name: jenkinsUsername | ||||
|         type: string | ||||
|         description: "The jenkins username" | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         secret: true | ||||
|         aliases: | ||||
|           - name: userId | ||||
|         resourceRef: | ||||
|           - type: vaultSecret | ||||
|             paths: | ||||
|               - $(vaultPath)/jenkins | ||||
|               - $(vaultBasePath)/$(vaultPipelineName)/jenkins | ||||
|               - $(vaultBasePath)/GROUP-SECRETS/jenkins | ||||
|       - name: jenkinsToken | ||||
|         type: string | ||||
|         description: "The jenkins token" | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         secret: true | ||||
|         aliases: | ||||
|           - name: token | ||||
|         resourceRef: | ||||
|           - type: vaultSecret | ||||
|             paths: | ||||
|               - $(vaultPath)/jenkins | ||||
|               - $(vaultBasePath)/$(vaultPipelineName)/jenkins | ||||
|               - $(vaultBasePath)/GROUP-SECRETS/jenkins | ||||
|       - name: vaultAppRoleSecretTokenCredentialsId | ||||
|         type: string | ||||
|         description: The Jenkins credential ID for the Vault AppRole Secret ID credential | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         mandatory: true | ||||
|       - name: vaultServerUrl | ||||
|         type: string | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         description: The URL for the Vault server to use | ||||
|         mandatory: true | ||||
|       - name: vaultNamespace | ||||
|         type: string | ||||
|         scope: | ||||
|           - GENERAL | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         description: The vault namespace that should be used (optional) | ||||
|       - name: daysBeforeExpiry | ||||
|         type: int | ||||
|         description: The amount of days before expiry until the secret ID gets rotated | ||||
|         scope: | ||||
|           - PARAMETERS | ||||
|           - STAGES | ||||
|           - STEPS | ||||
|         default: 15 | ||||
| @@ -55,7 +55,7 @@ public class CommonStepsTest extends BasePiperTest{ | ||||
|             'piperPipeline', | ||||
|             'prepareDefaultValues', | ||||
|             'runClosures', | ||||
|             'setupCommonPipelineEnvironment' | ||||
|             'setupCommonPipelineEnvironment', | ||||
|         ] | ||||
|  | ||||
|         List steps = getSteps().stream() | ||||
| @@ -166,7 +166,8 @@ public class CommonStepsTest extends BasePiperTest{ | ||||
|         'containerSaveImage', //implementing new golang pattern without fields | ||||
|         'detectExecuteScan', //implementing new golang pattern without fields | ||||
|         'kanikoExecute', //implementing new golang pattern without fields | ||||
|         'gitopsUpdateDeployment' //implementing new golang pattern without fields | ||||
|         'gitopsUpdateDeployment', //implementing new golang pattern without fields | ||||
|         'vaultRotateSecretId' //implementing new golang pattern without fields | ||||
|     ] | ||||
|  | ||||
|     @Test | ||||
|   | ||||
| @@ -8,9 +8,8 @@ import static com.sap.piper.Prerequisites.checkScript | ||||
|  | ||||
| @Field String STEP_NAME = getClass().getName() | ||||
| @Field String TECHNICAL_STAGE_NAME = 'postPipelineHook' | ||||
|  | ||||
| @Field Set GENERAL_CONFIG_KEYS = [] | ||||
| @Field STAGE_STEP_KEYS = [] | ||||
| @Field Set GENERAL_CONFIG_KEYS = ["vaultServerUrl", "vaultAppRoleTokenCredentialsId", "vaultAppRoleSecretTokenCredentialsId"] | ||||
| @Field STAGE_STEP_KEYS = ["vaultRotateSecretId"] | ||||
| @Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus(STAGE_STEP_KEYS) | ||||
| @Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS | ||||
|  | ||||
| @@ -18,6 +17,7 @@ import static com.sap.piper.Prerequisites.checkScript | ||||
|  * In this stage reporting actions like mail notification or telemetry reporting are executed. | ||||
|  * | ||||
|  * This stage contains following steps: | ||||
|  * - [vaultRotateSecretId](./vaultRotateSecretId.md) | ||||
|  * - [influxWriteData](./influxWriteData.md) | ||||
|  * - [mailSendNotification](./mailSendNotification.md) | ||||
|  * | ||||
| @@ -37,14 +37,20 @@ void call(Map parameters = [:]) { | ||||
|         .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) | ||||
|         .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS) | ||||
|         .mixin(parameters, PARAMETER_KEYS) | ||||
|         .addIfEmpty("vaultRotateSecretId", false) | ||||
|         .use() | ||||
|  | ||||
|     piperStageWrapper (script: script, stageName: stageName, stageLocking: false) { | ||||
|         // rotate vault secret id if necessary | ||||
|         if (config.vaultRotateSecretId && config.vaultServerUrl && config.vaultAppRoleSecretTokenCredentialsId | ||||
|             && config.vaultAppRoleTokenCredentialsId) { | ||||
|             vaultRotateSecretId script: script | ||||
|         } | ||||
|  | ||||
|         // telemetry reporting | ||||
|         utils.pushToSWA([step: STEP_NAME], config) | ||||
|  | ||||
|         influxWriteData script: script | ||||
|  | ||||
|         if(env.BRANCH_NAME == parameters.script.commonPipelineEnvironment.getStepConfiguration('', '').productiveBranch) { | ||||
|             if(parameters.script.commonPipelineEnvironment.configuration.runStep?.get('Post Actions')?.slackSendNotification) { | ||||
|                 slackSendNotification script: parameters.script | ||||
|   | ||||
							
								
								
									
										10
									
								
								vars/vaultRotateSecretId.groovy
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								vars/vaultRotateSecretId.groovy
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import groovy.transform.Field | ||||
| import static com.sap.piper.Prerequisites.checkScript | ||||
|  | ||||
| @Field String STEP_NAME = getClass().getName() | ||||
| @Field String METADATA_FILE = 'metadata/vaultRotateSecretId.yaml' | ||||
|  | ||||
| void call(Map parameters = [:]) { | ||||
|         def script = checkScript(this, parameters) ?: this | ||||
|         piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, [], false, false, false) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user