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(trustengine): Integrate Trust Engine into step config resolver #5032

Merged
merged 32 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
13513f4
trust engine config and handelling for vault
anilkeshav27 Aug 12, 2024
94ee8dd
add function for resolving trust engine reference
Aug 19, 2024
cf29264
refactor
Aug 21, 2024
6be1aae
add basic test
Aug 21, 2024
bf177cb
adapt to new trust engine response format
Aug 28, 2024
2f7d5a8
Merge branch 'master' into jliempt/trustEngine
Aug 28, 2024
6a1a209
remove accidental cyclic dependency
Aug 28, 2024
4d73ff8
move trust engine hook config
Aug 29, 2024
22bfc90
refactor by separating code from vault
Aug 29, 2024
cddb5fb
move trust engine files to own pkg
Aug 29, 2024
aee60ec
adapt to changes of previous commit
Aug 29, 2024
e279827
log full error response of trust engine API
Aug 29, 2024
868c086
enable getting multiple tokens from trustengine
Aug 30, 2024
66fd18c
remove comment
Aug 30, 2024
cf5aaec
incorporate review comments
Aug 30, 2024
58b8b9f
go generate
Aug 30, 2024
8047610
Merge branch 'master' into jliempt/trustEngine
jliempt Aug 30, 2024
3079123
update unit tests
Sep 2, 2024
2c439b7
apply suggested changes from code review
Sep 2, 2024
810ec45
fix unit tests
Sep 2, 2024
fe7f651
add unit tests for config pkg
Sep 2, 2024
f77a552
make changes based on review comments
Sep 3, 2024
f0c6fcc
make trust engine token available in GeneralConfig and minor fixes
Sep 3, 2024
f3e3e46
fix error logic when reading trust engine hook
Sep 3, 2024
71be412
make getResponse more flexible and update logging
Sep 4, 2024
a6335ee
update resource reference format
Sep 5, 2024
d8a9f94
improve URL handling
Sep 5, 2024
ced08de
improve logging
Sep 6, 2024
1a5b2ba
use errors.Wrap() instead of errors.Join()
Sep 6, 2024
e20f268
update log messages based on suggestions
Sep 6, 2024
d605c2a
Merge branch 'master' into jliempt/trustEngine
jliempt Sep 11, 2024
c0bc216
remove trustengine resource ref from Sonar step
Sep 11, 2024
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
9 changes: 7 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,13 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri
}
}

c.SetTrustEngineConfiguration(stepConfig.HookConfig)
ResolveAllTrustEngineReferences(&stepConfig, append(parameters, ReportingParameters.Parameters...), c.trustEngineConfiguration)
err = c.SetTrustEngineConfiguration(stepConfig.HookConfig)
if err != nil {
trustengineClient := trustengine.PrepareClient(c.trustEngineConfiguration)
ResolveAllTrustEngineReferences(&stepConfig, append(parameters, ReportingParameters.Parameters...), c.trustEngineConfiguration, trustengineClient)
} else {
log.Entry().WithError(err).Warn("Trust Engine lookup skipped")
}

// finally do the condition evaluation post processing
for _, p := range parameters {
Expand Down
40 changes: 21 additions & 19 deletions pkg/config/trustengine.go
Original file line number Diff line number Diff line change
@@ -1,49 +1,51 @@
package config

import (
"fmt"
"errors"
"os"

piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/trustengine"
)

func ResolveAllTrustEngineReferences(config *StepConfig, params []StepParameters, trustEngineConfiguration trustengine.Configuration) {
// ResolveAllTrustEngineReferences retrieves all the step's secrets from the trust engine
func ResolveAllTrustEngineReferences(config *StepConfig, params []StepParameters, trustEngineConfiguration trustengine.Configuration, client *piperhttp.Client) {
for _, param := range params {
if ref := param.GetReference("trustengineSecret"); ref != nil {
CCFenner marked this conversation as resolved.
Show resolved Hide resolved
if config.Config[param.Name] == "" { // what if Jenkins set the secret?
jliempt marked this conversation as resolved.
Show resolved Hide resolved
resolveTrustEngineReference(ref, config, &piperhttp.Client{}, param, trustEngineConfiguration)
log.Entry().Infof("Getting %s from Trust Engine", ref.Name)
token, err := trustengine.GetToken(ref.Name, client, trustEngineConfiguration)
if err != nil {
log.Entry().Info(" failed")
log.Entry().WithError(err).Warnf("Couldn't get %s secret from Trust Engine", param.Name)
continue
}
log.RegisterSecret(token)
config.Config[param.Name] = token
log.Entry().Info(" succeeded")
}
}
}
}

// resolveTrustEngineReference retrieves a secret from Vault trust engine
func resolveTrustEngineReference(ref *ResourceReference, config *StepConfig, client *piperhttp.Client, param StepParameters, trustEngineConfiguration trustengine.Configuration) {
token, err := trustengine.GetToken(ref.Name, client, trustEngineConfiguration)
if err != nil {
log.Entry().Infof(fmt.Sprintf("couldn't get secret from trust engine: %s", err))
return
}
log.RegisterSecret(token)
config.Config[param.Name] = token
log.Entry().Infof("retrieving %s token from trust engine succeeded", ref.Name)
}

// SetTrustEngineConfiguration sets the server URL and token
func (c *Config) SetTrustEngineConfiguration(hookConfig map[string]interface{}) {
func (c *Config) SetTrustEngineConfiguration(hookConfig map[string]interface{}) error {
CCFenner marked this conversation as resolved.
Show resolved Hide resolved
c.trustEngineConfiguration = trustengine.Configuration{}
c.trustEngineConfiguration.Token = os.Getenv("PIPER_TRUST_ENGINE_TOKEN")
jliempt marked this conversation as resolved.
Show resolved Hide resolved
if len(c.trustEngineConfiguration.Token) == 0 {
log.Entry().Warn("No Trust Engine token environment variable set or is empty string")
}

trustEngineHook, ok := hookConfig["trustEngine"].(map[string]interface{})
trustEngineHook, ok := hookConfig["trustengine"].(map[string]interface{})
if !ok {
log.Entry().Debug("no trust engine hook configuration found")
return errors.New("no trust engine hook configuration found")
}
serverURL, ok := trustEngineHook["serverURL"].(string)
if ok {
c.trustEngineConfiguration.ServerURL = serverURL
CCFenner marked this conversation as resolved.
Show resolved Hide resolved
jliempt marked this conversation as resolved.
Show resolved Hide resolved
} else {
log.Entry().Debug("no server URL found in trust engine hook configuration")
return errors.New("no server URL found in trust engine hook configuration")
}
return nil
}
66 changes: 66 additions & 0 deletions pkg/config/trustengine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package config

import (
"fmt"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/trustengine"
"github.com/jarcoal/httpmock"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

const secretName = "token"
const secretNameInTrustEngine = "sonar"
const testBaseURL = "https://www.project-piper.io/tokens"
const mockSonarToken = "mockSonarToken"

var mockSingleTokenResponse = fmt.Sprintf("{\"sonar\": \"%s\"}", mockSonarToken)

func TestTrustEngineConfig(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, testBaseURL+"?systems=sonar", httpmock.NewStringResponder(200, mockSingleTokenResponse))

stepParams := []StepParameters{stepParam(secretName, "trustengineSecret", secretNameInTrustEngine, secretName)}

trustEngineConfiguration := trustengine.Configuration{
Token: "mockToken",
ServerURL: testBaseURL,
}
client := &piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})

t.Run("Load secret from Trust Engine - secret not set yet by Vault or config.yml", func(t *testing.T) {
stepConfig := &StepConfig{Config: map[string]interface{}{
secretName: "",
}}

ResolveAllTrustEngineReferences(stepConfig, stepParams, trustEngineConfiguration, client)
assert.Equal(t, mockSonarToken, stepConfig.Config[secretName])
})

t.Run("Load secret from Trust Engine - secret already by Vault or config.yml", func(t *testing.T) {
stepConfig := &StepConfig{Config: map[string]interface{}{
secretName: "aMockTokenFromVault",
}}

ResolveAllTrustEngineReferences(stepConfig, stepParams, trustEngineConfiguration, client)
assert.NotEqual(t, mockSonarToken, stepConfig.Config[secretName])
})
}

func stepParam(name, refType, vaultSecretNameProperty, defaultSecretNameName string) StepParameters {

Check failure on line 54 in pkg/config/trustengine_test.go

View workflow job for this annotation

GitHub Actions / unit

other declaration of stepParam
return StepParameters{
Name: name,
Aliases: []Alias{},
ResourceRef: []ResourceReference{
{
Type: refType,
Name: vaultSecretNameProperty,
Default: defaultSecretNameName,
},
},
}
}
31 changes: 22 additions & 9 deletions pkg/trustengine/trustengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
)

type Secret struct {
Token string `json:"sonar,omitempty"`
System string `json:"system,omitempty"`
Token string
System string
}

type Response struct {
Expand All @@ -26,10 +26,11 @@ type Configuration struct {
Token string
}

// GetToken requests a single token
func GetToken(refName string, client *piperhttp.Client, trustEngineConfiguration Configuration) (string, error) {
secrets, err := GetSecrets([]string{refName}, client, trustEngineConfiguration)
if err != nil {
return "", err
return "", errors.Join(err, errors.New("couldn't get token from trust engine"))
jliempt marked this conversation as resolved.
Show resolved Hide resolved
}
for _, s := range secrets {
if s.System == refName {
Expand All @@ -39,11 +40,12 @@ func GetToken(refName string, client *piperhttp.Client, trustEngineConfiguration
return "", errors.New("could not find token in trust engine response")
}

// GetSecrets transforms the trust engine JSON response into trust engine secrets, and can be used to request multiple tokens
func GetSecrets(refNames []string, client *piperhttp.Client, trustEngineConfiguration Configuration) ([]Secret, error) {
var secrets []Secret
response, err := GetResponse(refNames, client, trustEngineConfiguration)
if err != nil {
return secrets, err
return secrets, errors.Join(err, errors.New("getting secrets from trust engine failed"))
}
for k, v := range response {
secrets = append(secrets, Secret{
Expand All @@ -54,30 +56,41 @@ func GetSecrets(refNames []string, client *piperhttp.Client, trustEngineConfigur
return secrets, nil
}

// GetResponse returns a map of the JSON response that the trust engine puts out
func GetResponse(refNames []string, client *piperhttp.Client, trustEngineConfiguration Configuration) (map[string]string, error) {
var secrets map[string]string
query := fmt.Sprintf("?systems=%s", strings.Join(refNames, ","))
fullURL := trustEngineConfiguration.ServerURL + query

log.Entry().Debugf("getting token from %s", fullURL)
var header http.Header = map[string][]string{"Authorization": {fmt.Sprintf("Bearer %s", trustEngineConfiguration.Token)}}
header := make(http.Header)
header.Add("Accept", "application/json")

log.Entry().Debugf("with URL %s", fullURL)
response, err := client.SendRequest(http.MethodGet, fullURL, nil, header, nil)
if err != nil && response != nil {
// the body contains full error message which we want to log
bodyBytes, bodyErr := io.ReadAll(response.Body)
jliempt marked this conversation as resolved.
Show resolved Hide resolved
if bodyErr == nil {
log.Entry().Info(string(bodyBytes))
err = errors.Join(err, errors.New(string(bodyBytes)))
}
}
if err != nil {
return secrets, err
return secrets, errors.Join(err, errors.New("getting response from trust engine failed"))
}
defer response.Body.Close()

err = json.NewDecoder(response.Body).Decode(&secrets)
if err != nil {
return secrets, err
return secrets, errors.Join(err, errors.New("getting response from trust engine failed"))
}

return secrets, nil
}

func PrepareClient(trustEngineConfiguration Configuration) *piperhttp.Client {
client := piperhttp.Client{}
CCFenner marked this conversation as resolved.
Show resolved Hide resolved
client.SetOptions(piperhttp.ClientOptions{
Token: fmt.Sprintf("Bearer %s", trustEngineConfiguration.Token),
})
return &client
}
61 changes: 54 additions & 7 deletions pkg/trustengine/trustengine_test.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,72 @@
package trustengine

import (
"net/url"
"fmt"
"github.com/jarcoal/httpmock"
"net/http"
"testing"

piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/stretchr/testify/assert"
)

const testBaseURL = "https://www.project-piper.io/"
const testFullURL = "https://www.project-piper.io/test"
const testBaseURL = "https://www.project-piper.io/tokens"
const mockSonarToken = "mockSonarToken"
const mockCumulusToken = "mockCumulusToken"
const errorMsg403 = "unauthorized to request token"

var mockSingleTokenResponse = fmt.Sprintf("{\"sonar\": \"%s\"}", mockSonarToken)
var mockTwoTokensResponse = fmt.Sprintf("{\"sonar\": \"%s\", \"cumulus\": \"%s\"}", mockSonarToken, mockCumulusToken)
var trustEngineConfiguration = Configuration{
Token: "testToken",
ServerURL: testBaseURL,
}

func TestTrustEngine(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()

t.Run("Get Sonar token - happy path", func(t *testing.T) {
httpmock.RegisterResponder(http.MethodGet, testBaseURL+"?systems=sonar", httpmock.NewStringResponder(200, mockSingleTokenResponse))

client := &piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})

token, err := GetToken("sonar", client, trustEngineConfiguration)
assert.NoError(t, err)
assert.Equal(t, mockSonarToken, token)
})

t.Run("Get multiple tokens - happy path", func(t *testing.T) {
httpmock.RegisterResponder(http.MethodGet, testBaseURL+"?systems=sonar,cumulus", httpmock.NewStringResponder(200, mockTwoTokensResponse))

client := &piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})

secrets, err := GetSecrets([]string{"sonar", "cumulus"}, client, trustEngineConfiguration)

assert.NoError(t, err)
assert.Len(t, secrets, 2)
for _, s := range secrets {
switch system := s.System; system {
case "sonar":
assert.Equal(t, mockSonarToken, s.Token)
case "cumulus":
assert.Equal(t, mockCumulusToken, s.Token)
default:
continue
}
}
})

t.Run("Test getting Sonar token", func(t *testing.T) {
t.Parallel()
t.Run("Get Sonar token - 403 error", func(t *testing.T) {
httpmock.RegisterResponder(http.MethodGet, testBaseURL+"?systems=sonar", httpmock.NewStringResponder(403, errorMsg403))

client := &piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})

url, _ := url.Parse(testBaseURL)
GetTrustEngineSecret(url, "test", "123", client)
_, err := GetToken("sonar", client, trustEngineConfiguration)
assert.Error(t, err)
})

}
Loading