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 21 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
14 changes: 10 additions & 4 deletions cmd/piper.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type GeneralConfigOptions struct {
VaultServerURL string
VaultNamespace string
VaultPath string
TrustEngineToken string
HookConfig HookConfiguration
MetaDataResolver func() map[string]config.StepData
GCPJsonKeyFilePath string
Expand All @@ -51,10 +52,11 @@ type GeneralConfigOptions struct {

// HookConfiguration contains the configuration for supported hooks, so far Sentry and Splunk are supported.
type HookConfiguration struct {
SentryConfig SentryConfiguration `json:"sentry,omitempty"`
SplunkConfig SplunkConfiguration `json:"splunk,omitempty"`
PendoConfig PendoConfiguration `json:"pendo,omitempty"`
OIDCConfig OIDCConfiguration `json:"oidc,omitempty"`
SentryConfig SentryConfiguration `json:"sentry,omitempty"`
SplunkConfig SplunkConfiguration `json:"splunk,omitempty"`
PendoConfig PendoConfiguration `json:"pendo,omitempty"`
OIDCConfig OIDCConfiguration `json:"oidc,omitempty"`
TrustEngineConfig TrustEngineConfiguration `json:"trustEngine,omitempty"`
}

// SentryConfiguration defines the configuration options for the Sentry logging system
Expand Down Expand Up @@ -82,6 +84,10 @@ type OIDCConfiguration struct {
RoleID string `json:",roleID,omitempty"`
}

type TrustEngineConfiguration struct {
ServerURL string `json:",baseURL,omitempty"`
}

var rootCmd = &cobra.Command{
Use: "piper",
Short: "Executes CI/CD steps from project 'Piper' ",
Expand Down
5 changes: 5 additions & 0 deletions cmd/sonarExecuteScan_generated.go

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

31 changes: 21 additions & 10 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"regexp"
"strings"

"github.com/SAP/jenkins-library/pkg/trustengine"

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

Expand All @@ -21,16 +23,17 @@ import (

// Config defines the structure of the config files
type Config struct {
CustomDefaults []string `json:"customDefaults,omitempty"`
General map[string]interface{} `json:"general"`
Stages map[string]map[string]interface{} `json:"stages"`
Steps map[string]map[string]interface{} `json:"steps"`
Hooks map[string]interface{} `json:"hooks,omitempty"`
defaults PipelineDefaults
initialized bool
accessTokens map[string]string
openFile func(s string, t map[string]string) (io.ReadCloser, error)
vaultCredentials VaultCredentials
CustomDefaults []string `json:"customDefaults,omitempty"`
General map[string]interface{} `json:"general"`
Stages map[string]map[string]interface{} `json:"stages"`
Steps map[string]map[string]interface{} `json:"steps"`
Hooks map[string]interface{} `json:"hooks,omitempty"`
defaults PipelineDefaults
initialized bool
accessTokens map[string]string
openFile func(s string, t map[string]string) (io.ReadCloser, error)
vaultCredentials VaultCredentials
trustEngineConfiguration trustengine.Configuration
}

// StepConfig defines the structure for merged step configuration
Expand Down Expand Up @@ -270,6 +273,14 @@ func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON stri
}
}

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 {
if len(p.Conditions) > 0 {
Expand Down
51 changes: 51 additions & 0 deletions pkg/config/trustengine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config

import (
"errors"
"os"

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

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

// SetTrustEngineConfiguration sets the server URL and token
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{})
if !ok {
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
} else {
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,
},
},
}
}
96 changes: 96 additions & 0 deletions pkg/trustengine/trustengine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package trustengine

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"

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

type Secret struct {
Token string
System string
}

type Response struct {
Secrets []Secret
}

type Configuration struct {
ServerURL string
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 "", 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 {
return s.Token, nil
}
}
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, errors.Join(err, errors.New("getting secrets from trust engine failed"))
}
for k, v := range response {
secrets = append(secrets, Secret{
System: k,
Token: v})
}

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

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 {
err = errors.Join(err, errors.New(string(bodyBytes)))
}
}
if err != nil {
return secrets, errors.Join(err, errors.New("getting response from trust engine failed"))
}
jliempt marked this conversation as resolved.
Show resolved Hide resolved
defer response.Body.Close()

err = json.NewDecoder(response.Body).Decode(&secrets)
if err != nil {
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
}
72 changes: 72 additions & 0 deletions pkg/trustengine/trustengine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package trustengine

import (
"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/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("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})

_, err := GetToken("sonar", client, trustEngineConfiguration)
assert.Error(t, err)
})

}
2 changes: 2 additions & 0 deletions resources/metadata/sonarExecuteScan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ spec:
default: sonar
- name: sonarTokenCredentialsId
type: secret
- name: sonar
type: trustengineSecret
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably do this in a separate PR, so that the new functionality doesn't get activated yet.

aliases:
- name: sonarToken
- name: organization
Expand Down
Loading