1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-11-24 08:32:32 +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:
Kevin Stiehl 2020-11-17 13:49:31 +01:00 committed by GitHub
parent 8653780cf6
commit dfab156427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1188 additions and 73 deletions

View File

@ -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
View 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
}

View 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
}

View 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")
}

View 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,
}
}

View File

@ -0,0 +1,17 @@
# ${docGenStepName}
## ${docGenDescription}
## Prerequisites
## ${docGenParameters}
## ${docGenConfiguration}
## ${docJenkinsPluginDependencies}
## Exceptions
none
## Examples

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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
View 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)
}

View 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")
})
}

View 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
}

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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
}

View 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

View File

@ -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

View File

@ -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

View 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)
}