Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Axon Relay currently supports access to:
* Jira
* SonarQube
* Prometheus
* Harness

For details on Relay, see [here](README.relay.md).

Expand Down
1 change: 1 addition & 0 deletions README.relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Generally the naming works like:
| **Bitbucket Hosted** | `BITBUCKET_API=https://bitbucket.mycompany.com`, `BITBUCKET_USERNAME`, `BITBUCKET_PASSWORD` |
| **Jira** | `JIRA_API=https://jira.mycompany.com`, `JIRA_USERNAME`, `JIRA_TOKEN` |
| **Jira Bearer/Cloud** | Arg `-s bearer`, `JIRA_API=https://mycompany.atlassian.com`, `JIRA_TOKEN` |
| **Harness** | `HARNESS_API=https://app.harness.io`, `HARNESS_TOKEN` |

## How it works

Expand Down
3 changes: 2 additions & 1 deletion agent/common/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
IntegrationSonarqube Integration = "sonarqube"
IntegrationBitbucket Integration = "bitbucket"
IntegrationPrometheus Integration = "prometheus"
IntegrationHarness Integration = "harness"
)

var subtypes = map[Integration][]string{
Expand Down Expand Up @@ -59,7 +60,7 @@ func ParseIntegration(s string) (Integration, error) {
}

func ValidIntegrations() []Integration {
return []Integration{IntegrationCustom, IntegrationGithub, IntegrationJira, IntegrationGitlab, IntegrationBitbucket, IntegrationSonarqube, IntegrationPrometheus}
return []Integration{IntegrationCustom, IntegrationGithub, IntegrationJira, IntegrationGitlab, IntegrationBitbucket, IntegrationSonarqube, IntegrationPrometheus, IntegrationHarness}
}

type IntegrationInfo struct {
Expand Down
34 changes: 34 additions & 0 deletions agent/common/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,40 @@ func TestLoadIntegrationAcceptFilePoolVars(t *testing.T) {
require.NotContains(t, string(contents), "GITHUB_TOKEN_POOL")
}

func TestLoadIntegrationAcceptFileHarness(t *testing.T) {
os.Setenv("HARNESS_API", "https://app.harness.io")
os.Setenv("HARNESS_TOKEN", "the-harness-token")

acceptFile, err := loadAcceptFile(t, IntegrationHarness)
require.NoError(t, err)
contents, err := acceptFile.Render(zap.NewNop())
require.NoError(t, err)
require.Contains(t, string(contents), `"x-api-key":"${HARNESS_TOKEN}"`)
require.Contains(t, string(contents), `"origin":"${HARNESS_API}"`)
}

func TestLoadIntegrationAcceptFileHarnessMissingVars(t *testing.T) {
os.Setenv("HARNESS_API", "")
os.Setenv("HARNESS_TOKEN", "")

acceptFile, err := loadAcceptFile(t, IntegrationHarness)
require.Error(t, err)
require.Contains(t, err.Error(), "HARNESS_API")
require.Empty(t, acceptFile)
}

func TestLoadValidationParamsHarness(t *testing.T) {
ii := IntegrationInfo{
Integration: IntegrationHarness,
}

validationParams := ii.GetValidationConfig()
require.NotNil(t, validationParams)
require.Equal(t, "$HARNESS_API/ng/api/user/currentUser", validationParams.URL)
require.Equal(t, "header", validationParams.Auth.Type)
require.Equal(t, "$HARNESS_TOKEN", validationParams.Auth.Value)
}

func TestGetOrigin(t *testing.T) {

os.Setenv("USER", "testuser")
Expand Down
12 changes: 12 additions & 0 deletions agent/server/snykbroker/accept_files/accept.harness.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": [
{
"method": "any",
"path": "/*",
"origin": "${HARNESS_API}",
"headers": {
"x-api-key": "${HARNESS_TOKEN}"
}
}
]
}
11 changes: 11 additions & 0 deletions agent/server/snykbroker/accept_files/config.harness.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"validation": [
{
"url": "$HARNESS_API/ng/api/user/currentUser",
"auth": {
"type": "header",
"value": "$HARNESS_TOKEN"
}
}
]
}
96 changes: 96 additions & 0 deletions agent/server/snykbroker/reflector_headers_harness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package snykbroker

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/cortexapps/axon/config"
"github.com/cortexapps/axon/server/snykbroker/acceptfile"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

// Tests the shipped Harness accept file end-to-end through the reflector:
// the x-api-key header is injected from HARNESS_TOKEN on proxied requests,
// and an inbound placeholder x-api-key is replaced (not duplicated).
func TestHarnessAcceptFileInjectsApiKeyHeader(t *testing.T) {

content, err := os.ReadFile("accept_files/accept.harness.json")
require.NoError(t, err)

var receivedApiKeys []string
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedApiKeys = r.Header.Values("x-api-key")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
}))
defer backendServer.Close()

os.Setenv("HARNESS_API", backendServer.URL)
os.Setenv("HARNESS_TOKEN", "real-harness-api-key")
defer func() {
os.Unsetenv("HARNESS_API")
os.Unsetenv("HARNESS_TOKEN")
}()

logger := zap.NewNop()
cfg := config.AgentConfig{
HttpRelayReflectorMode: config.RelayReflectorAllTraffic,
}
reflector := NewRegistrationReflector(RegistrationReflectorParams{
Logger: logger,
Config: cfg,
})

_, err = reflector.Start()
require.NoError(t, err)
defer reflector.Stop()

af, err := acceptfile.NewAcceptFile(content, cfg, nil)
require.NoError(t, err)

var proxyURI string
_, err = af.Render(
logger,
func(renderContext acceptfile.RenderContext) error {
for _, entry := range renderContext.AcceptFile.PrivateRules() {
originalURI := entry.Origin()
if originalURI == cfg.HttpBaseUrl() {
continue
}
newURI := reflector.ProxyURI(originalURI, WithHeadersResolver(entry.Headers()))
entry.SetOrigin(newURI)
proxyURI = newURI
}
return nil
},
)
require.NoError(t, err)
require.NotEmpty(t, proxyURI)

t.Run("injects x-api-key when absent", func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf("%s/ng/api/projects", proxyURI))
require.NoError(t, err)
defer resp.Body.Close()

require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, []string{"real-harness-api-key"}, receivedApiKeys)
})

t.Run("replaces inbound placeholder x-api-key", func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/ng/api/projects", proxyURI), nil)
require.NoError(t, err)
req.Header.Set("x-api-key", "placeholder-from-cortex")

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()

require.Equal(t, http.StatusOK, resp.StatusCode)
// exactly one value: the placeholder is replaced, not appended to
require.Equal(t, []string{"real-harness-api-key"}, receivedApiKeys)
})
}
Loading