diff --git a/.mockery.yaml b/.mockery.yaml index 74691b1cb9..aa8812cd9b 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -13,3 +13,8 @@ packages: dir: pkg/influx/mocks interfaces: WriteAPIBlocking: + github.com/SAP/jenkins-library/pkg/config: + config: + dir: pkg/config/mocks + interfaces: + VaultClient: diff --git a/cmd/gcpPublishEvent.go b/cmd/gcpPublishEvent.go index 1f8a52e78c..3ae5eb5b39 100644 --- a/cmd/gcpPublishEvent.go +++ b/cmd/gcpPublishEvent.go @@ -1,11 +1,13 @@ package cmd import ( + piperConfig "github.com/SAP/jenkins-library/pkg/config" "github.com/SAP/jenkins-library/pkg/events" "github.com/SAP/jenkins-library/pkg/gcp" "github.com/SAP/jenkins-library/pkg/log" "github.com/SAP/jenkins-library/pkg/orchestrator" "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/SAP/jenkins-library/pkg/vault" "github.com/pkg/errors" ) @@ -19,6 +21,7 @@ type gcpPublishEventUtils interface { type gcpPublishEventUtilsBundle struct { config *gcpPublishEventOptions + *vault.Client } func (g gcpPublishEventUtilsBundle) GetConfig() *gcpPublishEventOptions { @@ -33,17 +36,34 @@ func (g gcpPublishEventUtilsBundle) Publish(projectNumber string, topic string, return gcp.Publish(projectNumber, topic, token, key, data) } -// to be implemented through another PR! -func (g gcpPublishEventUtilsBundle) GetOIDCTokenByValidation(roleID string) (string, error) { - return "testToken", nil -} - func gcpPublishEvent(config gcpPublishEventOptions, telemetryData *telemetry.CustomData) { + vaultCreds := piperConfig.VaultCredentials{ + AppRoleID: GeneralConfig.VaultRoleID, + AppRoleSecretID: GeneralConfig.VaultRoleSecretID, + VaultToken: GeneralConfig.VaultToken, + } + vaultConfig := map[string]interface{}{ + "vaultNamespace": config.VaultNamespace, + "vaultServerUrl": config.VaultServerURL, + } + + client, err := piperConfig.GetVaultClientFromConfig(vaultConfig, vaultCreds) + if err != nil { + log.Entry().WithError(err).Warnf("could not create Vault client") + } + defer client.MustRevokeToken() + + vaultClient, ok := client.(vault.Client) + if !ok { + log.Entry().WithError(err).Warnf("could not create Vault client") + } + utils := gcpPublishEventUtilsBundle{ config: &config, + Client: &vaultClient, } - err := runGcpPublishEvent(utils) + err = runGcpPublishEvent(utils) if err != nil { // do not fail the step log.Entry().WithError(err).Warnf("step execution failed") @@ -66,10 +86,7 @@ func runGcpPublishEvent(utils gcpPublishEventUtils) error { return errors.Wrap(err, "failed to create event data") } - // this is currently returning a mock token. function will be implemented through another PR! - // roleID will come from GeneralConfig.HookConfig.OIDCConfig.RoleID - roleID := "test" - oidcToken, err := utils.GetOIDCTokenByValidation(roleID) + oidcToken, err := utils.GetOIDCTokenByValidation(GeneralConfig.HookConfig.OIDCConfig.RoleID) if err != nil { return errors.Wrap(err, "failed to get OIDC token") } diff --git a/cmd/piper.go b/cmd/piper.go index 1ce4f5089f..91c9ea0b40 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -54,6 +54,7 @@ type HookConfiguration struct { SentryConfig SentryConfiguration `json:"sentry,omitempty"` SplunkConfig SplunkConfiguration `json:"splunk,omitempty"` PendoConfig PendoConfiguration `json:"pendo,omitempty"` + OIDCConfig OIDCConfiguration `json:"oidc,omitempty"` } // SentryConfiguration defines the configuration options for the Sentry logging system @@ -76,6 +77,11 @@ type PendoConfiguration struct { Token string `json:"token,omitempty"` } +// OIDCConfiguration defines the configuration options for the OpenID Connect authentication system +type OIDCConfiguration struct { + RoleID string `json:",roleID,omitempty"` +} + var rootCmd = &cobra.Command{ Use: "piper", Short: "Executes CI/CD steps from project 'Piper' ", diff --git a/pkg/config/config.go b/pkg/config/config.go index e1dd4addd3..14f65686a8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -257,7 +257,7 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri // check whether vault should be skipped if skip, ok := stepConfig.Config["skipVault"].(bool); !ok || !skip { // fetch secrets from vault - vaultClient, err := getVaultClientFromConfig(stepConfig, c.vaultCredentials) + vaultClient, err := GetVaultClientFromConfig(stepConfig.Config, c.vaultCredentials) if err != nil { return StepConfig{}, err } diff --git a/pkg/config/mocks/vaultClient.go b/pkg/config/mocks/vaultClient.go index 7fe66c41db..3b6b8f9adb 100644 --- a/pkg/config/mocks/vaultClient.go +++ b/pkg/config/mocks/vaultClient.go @@ -1,19 +1,35 @@ -// Code generated by mockery v2.3.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package mocks import mock "github.com/stretchr/testify/mock" -// VaultMock is an autogenerated mock type for the vaultClient type -type VaultMock struct { +// VaultClient is an autogenerated mock type for the VaultClient type +type VaultClient struct { mock.Mock } +type VaultClient_Expecter struct { + mock *mock.Mock +} + +func (_m *VaultClient) EXPECT() *VaultClient_Expecter { + return &VaultClient_Expecter{mock: &_m.Mock} +} + // GetKvSecret provides a mock function with given fields: _a0 -func (_m *VaultMock) GetKvSecret(_a0 string) (map[string]string, error) { +func (_m *VaultClient) GetKvSecret(_a0 string) (map[string]string, error) { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetKvSecret") + } + var r0 map[string]string + var r1 error + if rf, ok := ret.Get(0).(func(string) (map[string]string, error)); ok { + return rf(_a0) + } if rf, ok := ret.Get(0).(func(string) map[string]string); ok { r0 = rf(_a0) } else { @@ -22,7 +38,62 @@ func (_m *VaultMock) GetKvSecret(_a0 string) (map[string]string, error) { } } + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// VaultClient_GetKvSecret_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKvSecret' +type VaultClient_GetKvSecret_Call struct { + *mock.Call +} + +// GetKvSecret is a helper method to define mock.On call +// - _a0 string +func (_e *VaultClient_Expecter) GetKvSecret(_a0 interface{}) *VaultClient_GetKvSecret_Call { + return &VaultClient_GetKvSecret_Call{Call: _e.mock.On("GetKvSecret", _a0)} +} + +func (_c *VaultClient_GetKvSecret_Call) Run(run func(_a0 string)) *VaultClient_GetKvSecret_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *VaultClient_GetKvSecret_Call) Return(_a0 map[string]string, _a1 error) *VaultClient_GetKvSecret_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *VaultClient_GetKvSecret_Call) RunAndReturn(run func(string) (map[string]string, error)) *VaultClient_GetKvSecret_Call { + _c.Call.Return(run) + return _c +} + +// GetOIDCTokenByValidation provides a mock function with given fields: _a0 +func (_m *VaultClient) GetOIDCTokenByValidation(_a0 string) (string, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetOIDCTokenByValidation") + } + + var r0 string var r1 error + if rf, ok := ret.Get(0).(func(string) (string, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(_a0) } else { @@ -32,7 +103,76 @@ func (_m *VaultMock) GetKvSecret(_a0 string) (map[string]string, error) { return r0, r1 } +// VaultClient_GetOIDCTokenByValidation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOIDCTokenByValidation' +type VaultClient_GetOIDCTokenByValidation_Call struct { + *mock.Call +} + +// GetOIDCTokenByValidation is a helper method to define mock.On call +// - _a0 string +func (_e *VaultClient_Expecter) GetOIDCTokenByValidation(_a0 interface{}) *VaultClient_GetOIDCTokenByValidation_Call { + return &VaultClient_GetOIDCTokenByValidation_Call{Call: _e.mock.On("GetOIDCTokenByValidation", _a0)} +} + +func (_c *VaultClient_GetOIDCTokenByValidation_Call) Run(run func(_a0 string)) *VaultClient_GetOIDCTokenByValidation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *VaultClient_GetOIDCTokenByValidation_Call) Return(_a0 string, _a1 error) *VaultClient_GetOIDCTokenByValidation_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *VaultClient_GetOIDCTokenByValidation_Call) RunAndReturn(run func(string) (string, error)) *VaultClient_GetOIDCTokenByValidation_Call { + _c.Call.Return(run) + return _c +} + // MustRevokeToken provides a mock function with given fields: -func (_m *VaultMock) MustRevokeToken() { +func (_m *VaultClient) MustRevokeToken() { _m.Called() } + +// VaultClient_MustRevokeToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MustRevokeToken' +type VaultClient_MustRevokeToken_Call struct { + *mock.Call +} + +// MustRevokeToken is a helper method to define mock.On call +func (_e *VaultClient_Expecter) MustRevokeToken() *VaultClient_MustRevokeToken_Call { + return &VaultClient_MustRevokeToken_Call{Call: _e.mock.On("MustRevokeToken")} +} + +func (_c *VaultClient_MustRevokeToken_Call) Run(run func()) *VaultClient_MustRevokeToken_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *VaultClient_MustRevokeToken_Call) Return() *VaultClient_MustRevokeToken_Call { + _c.Call.Return() + return _c +} + +func (_c *VaultClient_MustRevokeToken_Call) RunAndReturn(run func()) *VaultClient_MustRevokeToken_Call { + _c.Call.Return(run) + return _c +} + +// NewVaultClient creates a new instance of VaultClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewVaultClient(t interface { + mock.TestingT + Cleanup(func()) +}) *VaultClient { + mock := &VaultClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/config/vault.go b/pkg/config/vault.go index 417156c191..a65ca174f0 100644 --- a/pkg/config/vault.go +++ b/pkg/config/vault.go @@ -75,10 +75,11 @@ type VaultCredentials struct { VaultToken string } -// vaultClient interface for mocking -type vaultClient interface { +// VaultClient interface for mocking +type VaultClient interface { GetKvSecret(string) (map[string]string, error) MustRevokeToken() + GetOIDCTokenByValidation(string) (string, error) } func (s *StepConfig) mixinVaultConfig(parameters []StepParameters, configs ...map[string]interface{}) { @@ -91,8 +92,8 @@ func (s *StepConfig) mixinVaultConfig(parameters []StepParameters, configs ...ma } } -func getVaultClientFromConfig(config StepConfig, creds VaultCredentials) (vaultClient, error) { - address, addressOk := config.Config["vaultServerUrl"].(string) +func GetVaultClientFromConfig(config map[string]interface{}, creds VaultCredentials) (VaultClient, error) { + address, addressOk := config["vaultServerUrl"].(string) // if vault isn't used it's not an error if !addressOk || creds.VaultToken == "" && (creds.AppRoleID == "" || creds.AppRoleSecretID == "") { log.Entry().Debug("Vault not configured") @@ -102,11 +103,11 @@ func getVaultClientFromConfig(config StepConfig, creds VaultCredentials) (vaultC log.Entry().Debugf(" with URL %s", address) namespace := "" // namespaces are only available in vault enterprise so using them should be optional - if config.Config["vaultNamespace"] != nil { - namespace = config.Config["vaultNamespace"].(string) + if config["vaultNamespace"] != nil { + namespace = config["vaultNamespace"].(string) log.Entry().Debugf(" with namespace %s", namespace) } - var client vaultClient + var client VaultClient var err error clientConfig := &vault.Config{Config: &api.Config{Address: address}, Namespace: namespace} if creds.VaultToken != "" { @@ -124,7 +125,7 @@ func getVaultClientFromConfig(config StepConfig, creds VaultCredentials) (vaultC return client, nil } -func resolveAllVaultReferences(config *StepConfig, client vaultClient, params []StepParameters) { +func resolveAllVaultReferences(config *StepConfig, client VaultClient, params []StepParameters) { for _, param := range params { if ref := param.GetReference("vaultSecret"); ref != nil { resolveVaultReference(ref, config, client, param) @@ -135,7 +136,7 @@ func resolveAllVaultReferences(config *StepConfig, client vaultClient, params [] } } -func resolveVaultReference(ref *ResourceReference, config *StepConfig, client vaultClient, param StepParameters) { +func resolveVaultReference(ref *ResourceReference, config *StepConfig, client VaultClient, param StepParameters) { vaultDisableOverwrite, _ := config.Config["vaultDisableOverwrite"].(bool) if _, ok := config.Config[param.Name].(string); vaultDisableOverwrite && ok { log.Entry().Debugf("Not fetching '%s' from Vault since it has already been set", param.Name) @@ -173,20 +174,20 @@ func resolveVaultReference(ref *ResourceReference, config *StepConfig, client va } } -func resolveVaultTestCredentialsWrapper(config *StepConfig, client vaultClient) { +func resolveVaultTestCredentialsWrapper(config *StepConfig, client VaultClient) { log.Entry().Infof("Resolving test credentials wrapper") resolveVaultCredentialsWrapperBase(config, client, vaultTestCredentialPath, vaultTestCredentialKeys, vaultTestCredentialEnvPrefix, resolveVaultTestCredentials) } -func resolveVaultCredentialsWrapper(config *StepConfig, client vaultClient) { +func resolveVaultCredentialsWrapper(config *StepConfig, client VaultClient) { log.Entry().Infof("Resolving credentials wrapper") resolveVaultCredentialsWrapperBase(config, client, vaultCredentialPath, vaultCredentialKeys, vaultCredentialEnvPrefix, resolveVaultCredentials) } func resolveVaultCredentialsWrapperBase( - config *StepConfig, client vaultClient, + config *StepConfig, client VaultClient, vaultCredPath, vaultCredKeys, vaultCredEnvPrefix string, - resolveVaultCredentials func(config *StepConfig, client vaultClient), + resolveVaultCredentials func(config *StepConfig, client VaultClient), ) { switch config.Config[vaultCredPath].(type) { case string: @@ -230,7 +231,7 @@ func resolveVaultCredentialsWrapperBase( } // resolve test credential keys and expose as environment variables -func resolveVaultTestCredentials(config *StepConfig, client vaultClient) { +func resolveVaultTestCredentials(config *StepConfig, client VaultClient) { credPath, pathOk := config.Config[vaultTestCredentialPath].(string) keys := getTestCredentialKeys(config) if !(pathOk && keys != nil) || credPath == "" || len(keys) == 0 { @@ -267,7 +268,7 @@ func resolveVaultTestCredentials(config *StepConfig, client vaultClient) { } } -func resolveVaultCredentials(config *StepConfig, client vaultClient) { +func resolveVaultCredentials(config *StepConfig, client VaultClient) { credPath, pathOk := config.Config[vaultCredentialPath].(string) keys := getCredentialKeys(config) if !(pathOk && keys != nil) || credPath == "" || len(keys) == 0 { @@ -449,7 +450,7 @@ func createTemporarySecretFile(namePattern string, content string) (string, erro return file.Name(), nil } -func lookupPath(client vaultClient, path string, param *StepParameters) *string { +func lookupPath(client VaultClient, path string, param *StepParameters) *string { log.Entry().Debugf(" with Vault path '%s'", path) secret, err := client.GetKvSecret(path) if err != nil { diff --git a/pkg/config/vault_test.go b/pkg/config/vault_test.go index 2e04e619c7..f340c4e88c 100644 --- a/pkg/config/vault_test.go +++ b/pkg/config/vault_test.go @@ -22,7 +22,7 @@ func TestVaultConfigLoad(t *testing.T) { const secretNameOverrideKey = "mySecretVaultSecretName" t.Parallel() t.Run("Load secret from vault", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", }} @@ -35,7 +35,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("Load secret from Vault with path override", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", secretNameOverrideKey: "overrideSecretName", @@ -49,7 +49,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("Secrets are not overwritten", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", secretName: "preset value", @@ -64,7 +64,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("Secrets can be overwritten", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", secretName: "preset value", @@ -78,7 +78,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("Error is passed through", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", }} @@ -89,7 +89,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("Secret doesn't exist", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", }} @@ -101,7 +101,7 @@ func TestVaultConfigLoad(t *testing.T) { t.Run("Alias names should be considered", func(t *testing.T) { aliasName := "alias" - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", }} @@ -115,7 +115,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("Search over multiple paths", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultBasePath": "team2", "vaultPath": "team1", @@ -131,7 +131,7 @@ func TestVaultConfigLoad(t *testing.T) { }) t.Run("No BasePath is stepConfig.Configured", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{}} stepParams := []StepParameters{stepParam(secretName, "vaultSecret", secretNameOverrideKey, secretName)} resolveAllVaultReferences(&stepConfig, vaultMock, stepParams) @@ -144,7 +144,7 @@ func TestVaultSecretFiles(t *testing.T) { const secretName = "testSecret" const secretNameOverrideKey = "mySecretVaultSecretName" t.Run("Test Vault Secret File Reference", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", }} @@ -164,7 +164,7 @@ func TestVaultSecretFiles(t *testing.T) { VaultSecretFileDirectory = "" t.Run("Test temporary secret file cleanup", func(t *testing.T) { - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", }} @@ -232,7 +232,7 @@ func TestResolveVaultTestCredentialsWrapper(t *testing.T) { t.Run("Default test credential prefix", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} envPrefix := "PIPER_TESTCREDENTIAL_" stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", @@ -272,7 +272,7 @@ func TestResolveVaultTestCredentialsWrapper(t *testing.T) { t.Run("Multiple test credential prefixes", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} envPrefixes := []interface{}{"TEST1_", "TEST2_"} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", @@ -313,7 +313,7 @@ func TestResolveVaultTestCredentialsWrapper(t *testing.T) { t.Run("Multiple custom general purpuse credential environment prefixes", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} envPrefixes := []interface{}{"CUSTOM1_", "CUSTOM2_"} stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", @@ -362,7 +362,7 @@ func TestResolveVaultTestCredentialsWrapper(t *testing.T) { t.Run("Custom general purpose credential prefix along with fixed standard prefix", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} standardEnvPrefix := "PIPER_VAULTCREDENTIAL_" stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", @@ -401,7 +401,7 @@ func TestResolveVaultTestCredentials(t *testing.T) { t.Run("Default test credential prefix", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} envPrefix := "PIPER_TESTCREDENTIAL_" stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", @@ -438,7 +438,7 @@ func TestResolveVaultTestCredentials(t *testing.T) { t.Run("Custom general purpose credential prefix along with fixed standard prefix", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} standardEnvPrefix := "PIPER_VAULTCREDENTIAL_" stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", @@ -474,7 +474,7 @@ func TestResolveVaultTestCredentials(t *testing.T) { t.Run("Custom test credential prefix", func(t *testing.T) { t.Parallel() // init - vaultMock := &mocks.VaultMock{} + vaultMock := &mocks.VaultClient{} envPrefix := "CUSTOM_CREDENTIAL_" stepConfig := StepConfig{Config: map[string]interface{}{ "vaultPath": "team1", diff --git a/pkg/vault/client.go b/pkg/vault/client.go index 76e1018c3d..f9350958b0 100644 --- a/pkg/vault/client.go +++ b/pkg/vault/client.go @@ -34,6 +34,12 @@ type logicalClient interface { Write(string, map[string]interface{}) (*api.Secret, error) } +type VaultCredentials struct { + AppRoleID string + AppRoleSecretID string + VaultToken string +} + // NewClient instantiates a Client and sets the specified token func NewClient(config *Config, token string) (Client, error) { if config == nil { diff --git a/pkg/vault/oidc.go b/pkg/vault/oidc.go new file mode 100644 index 0000000000..a5ad6b790f --- /dev/null +++ b/pkg/vault/oidc.go @@ -0,0 +1,84 @@ +package vault + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/SAP/jenkins-library/pkg/log" + "github.com/pkg/errors" +) + +type JwtPayload struct { + Expire int64 `json:"exp"` +} + +// getOIDCToken returns the generated OIDC token and sets it in the env +func (v Client) getOIDCToken(roleID string) (string, error) { + oidcPath := sanitizePath(path.Join("identity/oidc/token/", roleID)) + c := v.lClient + jwt, err := c.Read(oidcPath) + if err != nil { + return "", err + } + + token := jwt.Data["token"].(string) + log.RegisterSecret(token) + os.Setenv("PIPER_OIDCIdentityToken", token) + + return token, nil +} + +// getJWTTokenPayload returns the payload of the JWT token using base64 decoding +func getJWTTokenPayload(token string) ([]byte, error) { + parts := strings.Split(token, ".") + if len(parts) >= 2 { + substr := parts[1] + decodedBytes, err := base64.RawStdEncoding.DecodeString(substr) + if err != nil { + return nil, errors.Wrap(err, "JWT payload couldn't be decoded: %s") + } + return decodedBytes, nil + } + + return nil, fmt.Errorf("Not a valid JWT token") +} + +func oidcTokenIsValid(token string) bool { + payload, err := getJWTTokenPayload(token) + if err != nil { + log.Entry().Debugf("OIDC token couldn't be validated: %s", err) + return false + } + + var jwtPayload JwtPayload + err = json.Unmarshal(payload, &jwtPayload) + if err != nil { + log.Entry().Debugf("OIDC token couldn't be validated: %s", err) + return false + } + + expiryTime := time.Unix(jwtPayload.Expire, 0) + currentTime := time.Now() + + return expiryTime.After(currentTime) +} + +// GetOIDCTokenByValidation returns the token if token is expired then get a new token else return old token +func (v Client) GetOIDCTokenByValidation(roleID string) (string, error) { + token := os.Getenv("PIPER_OIDCIdentityToken") + if token != "" && oidcTokenIsValid(token) { + return token, nil + } + + token, err := v.getOIDCToken(roleID) + if token == "" || err != nil { + return "", errors.Wrap(err, "failed to get OIDC token") + } + + return token, nil +} diff --git a/pkg/vault/oidc_test.go b/pkg/vault/oidc_test.go new file mode 100644 index 0000000000..2e1a973305 --- /dev/null +++ b/pkg/vault/oidc_test.go @@ -0,0 +1,87 @@ +package vault + +import ( + "encoding/base64" + "fmt" + "testing" + "time" + + "github.com/SAP/jenkins-library/pkg/vault/mocks" + "github.com/hashicorp/vault/api" + "github.com/stretchr/testify/assert" +) + +func TestOIDC(t *testing.T) { + oidcPath := "identity/oidc/token/testRoleID" + mockPayload := base64.RawStdEncoding.EncodeToString([]byte("testOIDCtoken123")) + mockToken := fmt.Sprintf("hvs.%s", mockPayload) + + mockJwt := &api.Secret{ + Data: map[string]interface{}{ + "path": oidcPath, + "token": mockToken, + }, + } + + t.Run("Test getting OIDC token - token non-existent in env yet", func(t *testing.T) { + t.Parallel() + + // init + vaultMock := &mocks.VaultMock{} + client := Client{vaultMock, &Config{}} + vaultMock.On("Read", oidcPath).Return(mockJwt, nil) + + // run + token, err := client.GetOIDCTokenByValidation("testRoleID") + + // assert + assert.NoError(t, err) + assert.Equal(t, token, mockToken) + }) + + t.Run("Test getting OIDC token - token exists in env and is valid", func(t *testing.T) { + // init + // still valid for 10 minutes + expiryTime := time.Now().Local().Add(time.Minute * time.Duration(10)) + payload := fmt.Sprintf("{\"exp\": %d}", expiryTime.Unix()) + payloadB64 := base64.RawStdEncoding.EncodeToString([]byte(payload)) + token := fmt.Sprintf("hvs.%s", payloadB64) + + t.Setenv("PIPER_OIDCIdentityToken", token) + + vaultMock := &mocks.VaultMock{} + client := Client{vaultMock, &Config{}} + vaultMock.On("Read", oidcPath).Return(mockJwt, nil) + + // run + tokenResult, err := client.GetOIDCTokenByValidation("testRoleID") + + // assert + assert.Equal(t, token, tokenResult) + assert.NoError(t, err) + }) + + t.Run("Test getting OIDC token - token exists in env and is invalid", func(t *testing.T) { + //init + // expired 10 minutes ago (time is subtracted!) + expiryTime := time.Now().Add(-time.Minute * time.Duration(10)) + payload := fmt.Sprintf("{\"exp\": %d}", expiryTime.Unix()) + payloadB64 := base64.RawStdEncoding.EncodeToString([]byte(payload)) + token := fmt.Sprintf("hvs.%s", payloadB64) + + t.Setenv("PIPER_OIDCIdentityToken", token) + + vaultMock := &mocks.VaultMock{} + client := Client{vaultMock, &Config{}} + vaultMock.On("Read", oidcPath).Return(mockJwt, nil) + + // run + tokenResult, err := client.GetOIDCTokenByValidation("testRoleID") + + // assert + client.GetOIDCTokenByValidation("testRoleID") + assert.Equal(t, mockToken, tokenResult) + assert.NoError(t, err) + }) + +}