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
2 changes: 2 additions & 0 deletions internal/api/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func (a accountAPI) Create(ctx context.Context, i AccountCreateInput) (*Account,
Account Account
} `graphql:"addAccount(input: $input)"`
}
ctx = maskSecrets(ctx, i.SecurityContext)
input := map[string]any{"input": i}
if err := a.c.Mutate(ctx, &mutation, input); err != nil {
return nil, err
Expand All @@ -101,6 +102,7 @@ func (a accountAPI) Update(ctx context.Context, i AccountUpdateInput) (*Account,
Account Account
} `graphql:"updateAccount(input: $input)"`
}
ctx = maskSecrets(ctx, i.SecurityContext)
input := map[string]any{"input": i}
if err := a.c.Mutate(ctx, &mutation, input); err != nil {
return nil, err
Expand Down
2 changes: 2 additions & 0 deletions internal/api/account_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func (a accountDiscoveryAPI) UpsertAzure(ctx context.Context, input AccountDisco
AccountDiscovery AccountDiscovery
} `graphql:"upsertAzureAccountDiscovery(input: $input)"`
}
ctx = maskSecrets(ctx, input.ClientSecret)
variables := map[string]any{"input": input}
if err := a.c.Mutate(ctx, &mutation, variables); err != nil {
return nil, err
Expand All @@ -164,6 +165,7 @@ func (a accountDiscoveryAPI) UpsertGCP(ctx context.Context, input AccountDiscove
AccountDiscovery AccountDiscovery
} `graphql:"upsertGCPAccountDiscovery(input: $input)"`
}
ctx = maskSecrets(ctx, input.CredentialJSON)
variables := map[string]any{"input": input}
if err := a.c.Mutate(ctx, &mutation, variables); err != nil {
return nil, err
Expand Down
8 changes: 2 additions & 6 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
// package api provides access to the GraphQL API.
package api

import (
"context"
)

// API provides access to the GraphQL API.
type API struct {
Account accountAPI
Expand All @@ -33,8 +29,8 @@ type API struct {
}

// New creates an API wrapper.
func New(ctx context.Context, config ClientConfig) *API {
c := newClient(ctx, config)
func New(config ClientConfig) *API {
c := newClient(config)
return &API{
Account: accountAPI{c},
AccountDiscovery: accountDiscoveryAPI{c},
Expand Down
9 changes: 9 additions & 0 deletions internal/api/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ type BindingExecutionConfig struct {
Variables *string `graphql:"variables" json:"variables"`
}

func (c BindingExecutionConfig) securityContext() *string {
if c.SecurityContext == nil {
return nil
}
return &c.SecurityContext.Default
}

// BindingExecutionConfigDryRun holds the dry run configuration for a binding execution config.
type BindingExecutionConfigDryRun struct {
Default bool `graphql:"default" json:"default"`
Expand Down Expand Up @@ -158,6 +165,7 @@ func (a bindingAPI) Create(ctx context.Context, i BindingCreateInput) (*Binding,
Binding Binding
} `graphql:"addBinding(input: $input)"`
}
ctx = maskSecrets(ctx, i.ExecutionConfig.securityContext())
input := map[string]any{"input": i}
if err := a.c.Mutate(ctx, &mutation, input); err != nil {
return nil, err
Expand All @@ -173,6 +181,7 @@ func (a bindingAPI) Update(ctx context.Context, i BindingUpdateInput) (*Binding,
Binding Binding
} `graphql:"updateBinding(input: $input)"`
}
ctx = maskSecrets(ctx, i.ExecutionConfig.securityContext())
input := map[string]any{"input": i}
if err := a.c.Mutate(ctx, &mutation, input); err != nil {
return nil, err
Expand Down
32 changes: 26 additions & 6 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,29 @@ func (c *client) Mutate(ctx context.Context, m any, variables map[string]any) er
return nil
}

// maskSecrets returns a context whose logs show the given secrets only as a
// mask. Request bodies carry them JSON-encoded, so that form is masked too.
func maskSecrets(ctx context.Context, secrets ...*string) context.Context {
var masked []string
for _, s := range secrets {
if s == nil || *s == "" {
continue
}
masked = append(masked, *s)
if encoded, err := json.Marshal(*s); err == nil {
if e := string(encoded[1 : len(encoded)-1]); e != *s {
masked = append(masked, e)
}
}
}
if len(masked) == 0 {
return ctx
}
return tflog.MaskLogStrings(ctx, masked...)
}

// newClient returns a configured graphql Client.
func newClient(ctx context.Context, config ClientConfig) *client {
func newClient(config ClientConfig) *client {
tfLog := hclog.LevelFromString(os.Getenv("TF_LOG"))
logBody := tfLog == hclog.Debug || tfLog == hclog.Trace

Expand All @@ -60,7 +81,6 @@ func newClient(ctx context.Context, config ClientConfig) *client {
APIKey: config.APIKey,
Version: config.Version,
Base: &logTransport{
Ctx: ctx,
Base: http.DefaultTransport,
LogBody: logBody,
},
Expand Down Expand Up @@ -89,9 +109,9 @@ func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.Base.RoundTrip(req)
}

// logTransport is an http.Transport that logs requests/responses.
// logTransport is an http.Transport that logs requests/responses through the
// request's context, so the logs apply whatever masking the caller set up.
type logTransport struct {
Ctx context.Context
Base http.RoundTripper
LogBody bool
}
Expand All @@ -109,7 +129,7 @@ func (t *logTransport) RoundTrip(req *http.Request) (*http.Response, error) {
}

tflog.Debug(
t.Ctx,
req.Context(),
"Performing GraphQL request",
map[string]any{
"req_method": req.Method,
Expand All @@ -129,7 +149,7 @@ func (t *logTransport) RoundTrip(req *http.Request) (*http.Response, error) {
}

tflog.Debug(
t.Ctx,
req.Context(),
"Got GraphQL response",
map[string]any{
"req_method": req.Method,
Expand Down
37 changes: 37 additions & 0 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
package api

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/hashicorp/terraform-plugin-log/tflogtest"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -95,6 +99,39 @@ func TestErrorTransport_400WithoutGraphQLErrors(t *testing.T) {
assert.Equal(t, "not json", string(body))
}

func TestMutationMasksSecretsInDebugLogs(t *testing.T) {
t.Setenv("TF_LOG", "DEBUG")
apiKey := "plain-secret-api-key"
// JSON-encoding escapes this one, so the request body carries a form that
// differs from the value itself.
appKey := "app\"secret\nkey"

var reqBody string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
reqBody = string(body)
_, _ = w.Write([]byte(`{"data":{"updateDatadogIntegration":{"datadogIntegration":{"configured":true,"statusInfo":{"details":[]}},"problems":[]}}}`))
}))
defer server.Close()

var output bytes.Buffer
ctx := tflogtest.RootLogger(context.Background(), &output)
api := New(ClientConfig{Endpoint: server.URL, APIKey: "token"})
_, err := api.DatadogIntegration.Update(ctx, DatadogIntegrationInput{APIKey: &apiKey, AppKey: &appKey})
assert.NoError(t, err)
assert.Contains(t, reqBody, apiKey, "the request itself carries the secret")

entries, err := tflogtest.MultilineJSONDecode(&output)
assert.NoError(t, err)
assert.Len(t, entries, 2)
for _, entry := range entries {
logged := fmt.Sprint(entry["req_body"])
assert.Contains(t, logged, `"apiKey":"***"`)
assert.Contains(t, logged, `"appKey":"***"`)
assert.NotContains(t, logged, "secret")
}
}

func TestErrorTransport_NonBadRequest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down
11 changes: 11 additions & 0 deletions internal/api/configuration_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ func (a configurationProfileAPI) UpsertJira(ctx context.Context, input JiraConfi
Configuration ConfigurationProfile
} `graphql:"addJiraProfile(input: $input)"`
}
ctx = maskSecrets(ctx, &input.APIKey)
variables := map[string]any{
"input": jiraConfigurationInput{
JiraConfiguration: input,
Expand Down Expand Up @@ -437,6 +438,11 @@ func (a configurationProfileAPI) UpsertSlack(ctx context.Context, config SlackCo
Configuration ConfigurationProfile
} `graphql:"addSlackProfile(input: $input)"`
}
secrets := []*string{config.Token}
for _, w := range config.Webhooks {
secrets = append(secrets, &w.URL)
}
ctx = maskSecrets(ctx, secrets...)
variables := map[string]any{
"input": slackConfigurationInput{
SlackConfiguration: config,
Expand All @@ -463,6 +469,7 @@ func (a configurationProfileAPI) UpsertServiceNow(ctx context.Context, config Se
Configuration ConfigurationProfile
} `graphql:"addServiceNowProfile(input: $input)"`
}
ctx = maskSecrets(ctx, &config.Password)
variables := map[string]any{
"input": serviceNowConfigurationInput{
ServiceNowConfiguration: config,
Expand All @@ -489,6 +496,7 @@ func (a configurationProfileAPI) UpsertSymphony(ctx context.Context, config Symp
Configuration ConfigurationProfile
} `graphql:"addSymphonyProfile(input: $input)"`
}
ctx = maskSecrets(ctx, &config.PrivateKey)
variables := map[string]any{
"input": symphonyConfigurationInput{
SymphonyConfiguration: config,
Expand All @@ -515,6 +523,9 @@ func (a configurationProfileAPI) UpsertEmail(ctx context.Context, input EmailCon
Configuration ConfigurationProfile
} `graphql:"addEmailProfile(input: $input)"`
}
if input.SMTP != nil {
ctx = maskSecrets(ctx, input.SMTP.Password)
}
variables := map[string]any{
"input": emailConfigurationInput{
EmailConfiguration: input,
Expand Down
1 change: 1 addition & 0 deletions internal/api/datadog_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (a datadogIntegrationAPI) Update(ctx context.Context, input DatadogIntegrat
Problems []problem
} `graphql:"updateDatadogIntegration(input: $input)"`
}
ctx = maskSecrets(ctx, input.APIKey, input.AppKey)
if err := a.c.Mutate(ctx, &mutation, map[string]any{"input": input}); err != nil {
return nil, err
}
Expand Down
15 changes: 15 additions & 0 deletions internal/api/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ func (i *RepositoryConfigAuthInput) GetGraphQLType() string {
return "RepositoryConfigAuthInput"
}

func (i *RepositoryConfigAuthInput) secrets() []*string {
if i == nil {
return nil
}
var secrets []*string
for _, key := range []string{"authToken", "sshPrivateKey", "sshPassphrase"} {
if v, ok := i.m[key].(*string); ok {
secrets = append(secrets, v)
}
}
return secrets
}

type RepositoryCreateInput struct {
URL string `json:"url"`
Name string `json:"name"`
Expand Down Expand Up @@ -169,6 +182,7 @@ func (a repositoryAPI) Create(ctx context.Context, i RepositoryCreateInput) (*Re
Problems []problem
} `graphql:"addRepositoryConfig(input: $input)"`
}
ctx = maskSecrets(ctx, i.Auth.secrets()...)
if err := a.c.Mutate(ctx, &m, map[string]any{"input": i}); err != nil {
return nil, err
}
Expand All @@ -185,6 +199,7 @@ func (a repositoryAPI) Update(ctx context.Context, i RepositoryUpdateInput) (*Re
Problems []problem
} `graphql:"updateRepositoryConfig(input: $input)"`
}
ctx = maskSecrets(ctx, i.Auth.secrets()...)
if err := a.c.Mutate(ctx, &m, map[string]any{"input": i}); err != nil {
return nil, err
}
Expand Down
1 change: 0 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ func (p *stackletProvider) Configure(ctx context.Context, req provider.Configure

// Make provider data accessible to the Configure method of resources and data sources
providerData := providerdata.New(
ctx,
api.ClientConfig{
Endpoint: creds.Endpoint,
APIKey: creds.APIKey,
Expand Down
5 changes: 2 additions & 3 deletions internal/providerdata/providerdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package providerdata

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/datasource"
Expand All @@ -18,9 +17,9 @@ type providerData struct {
}

// New returns configured provider data.
func New(ctx context.Context, config api.ClientConfig) *providerData {
func New(config api.ClientConfig) *providerData {
return &providerData{
API: api.New(ctx, config),
API: api.New(config),
}
}

Expand Down
Loading