Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vault): Facilitate Vault OIDC token #4916

Merged
merged 8 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/piper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' ",
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/mocks/vaultClient.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 17 additions & 16 deletions pkg/config/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {
Expand All @@ -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")
Expand All @@ -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 != "" {
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions pkg/vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
84 changes: 84 additions & 0 deletions pkg/vault/oidc.go
Original file line number Diff line number Diff line change
@@ -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: %s")
jliempt marked this conversation as resolved.
Show resolved Hide resolved
}

return token, nil
}
56 changes: 56 additions & 0 deletions pkg/vault/oidc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package vault

import (
"encoding/base64"
"testing"

"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"
mockToken := base64.StdEncoding.EncodeToString([]byte("testOIDCtoken123"))

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

vaultMock := &mocks.VaultMock{}
client := Client{vaultMock, &Config{}}
vaultMock.On("Read", oidcPath).Return(mockJwt, nil)

token, err := client.GetOIDCTokenByValidation("testRoleID")

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) {
// t.Parallel()

// t.Setenv("PIPER_OIDCIdentityToken", "testOIDCtoken123")

// vaultMock := &mocks.VaultMock{}
// _ = Client{vaultMock, &Config{}}
// vaultMock.On("Read", oidcPath).Return(mockJwt, nil)
// })

// t.Run("Test getting OIDC token - token exists in env and is invalid", func(t *testing.T) {
// t.Parallel()

// t.Setenv("PIPER_OIDCIdentityToken", "testOIDCtoken123")

// vaultMock := &mocks.VaultMock{}
// _ = Client{vaultMock, &Config{}}
// vaultMock.On("Read", oidcPath).Return(mockJwt, nil)
// })

}
Loading