Skip to content

Commit d5d2b8c

Browse files
committed
chore: tests cleanup
1 parent 4176f50 commit d5d2b8c

4 files changed

Lines changed: 93 additions & 11 deletions

File tree

internal/gcphelper/config.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
)
2323

2424
const (
25+
// envNameGoogleApplicationCredentials names the file holding Application Default Credentials.
26+
envNameGoogleApplicationCredentials = "GOOGLE_APPLICATION_CREDENTIALS"
27+
2528
tokenURL = "https://oauth2.googleapis.com/token"
2629

2730
cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
@@ -134,9 +137,14 @@ func (b *GCPConfigBuilder) Build(
134137
return nil, err
135138
}
136139

137-
if envCreds != nil {
140+
switch {
141+
case envCreds != nil:
138142
clientOpts = append(clientOpts, envCreds)
139-
} else if gcpCfg != nil && gcpCfg.Credentials != "" {
143+
// GOOGLE_APPLICATION_CREDENTIALS named a file that read as empty. Fall through to the
144+
// ADC chain rather than to the remaining sources, which would authenticate as a
145+
// different identity than the one the user pointed at.
146+
case env[envNameGoogleApplicationCredentials] != "":
147+
case gcpCfg != nil && gcpCfg.Credentials != "":
140148
// Use credentials file from config
141149
credOpt, err := credentialsFileOption(v, gcpCfg.Credentials)
142150
if err != nil {
@@ -146,19 +154,19 @@ func (b *GCPConfigBuilder) Build(
146154
if credOpt != nil {
147155
clientOpts = append(clientOpts, credOpt)
148156
}
149-
} else if gcpCfg != nil && gcpCfg.AccessToken != "" {
157+
case gcpCfg != nil && gcpCfg.AccessToken != "":
150158
// Use access token from config
151159
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{
152160
AccessToken: gcpCfg.AccessToken,
153161
})
154162
clientOpts = append(clientOpts, option.WithTokenSource(tokenSource))
155-
} else if oauthAccessToken := env["GOOGLE_OAUTH_ACCESS_TOKEN"]; oauthAccessToken != "" {
163+
case env["GOOGLE_OAUTH_ACCESS_TOKEN"] != "":
156164
// Use OAuth access token from environment
157165
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{
158-
AccessToken: oauthAccessToken,
166+
AccessToken: env["GOOGLE_OAUTH_ACCESS_TOKEN"],
159167
})
160168
clientOpts = append(clientOpts, option.WithTokenSource(tokenSource))
161-
} else if env["GOOGLE_CREDENTIALS"] != "" {
169+
case env["GOOGLE_CREDENTIALS"] != "":
162170
// Use GOOGLE_CREDENTIALS from environment (can be file path or JSON content)
163171
clientOpt, err := createGCPCredentialsFromGoogleCredentialsEnv(ctx, v)
164172
if err != nil {
@@ -194,7 +202,7 @@ func (b *GCPConfigBuilder) Build(
194202
// GOOGLE_APPLICATION_CREDENTIALS variable in v's environment. Returns nil when
195203
// the variable is not set.
196204
func createGCPCredentialsFromEnv(v *venv.Venv) (option.ClientOption, error) {
197-
credentialsFile := v.Env["GOOGLE_APPLICATION_CREDENTIALS"]
205+
credentialsFile := v.Env[envNameGoogleApplicationCredentials]
198206
if credentialsFile == "" {
199207
return nil, nil
200208
}

internal/gcphelper/config_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,23 @@ func TestGcpConfigEmptyCredentialsFileFallsBackToADC(t *testing.T) {
292292
require.NoError(t, err)
293293
assert.Empty(t, clientOpts)
294294
}
295+
296+
// TestGcpConfigEmptyGACDoesNotFallBackToGoogleCredentials pins that an unpopulated
297+
// GOOGLE_APPLICATION_CREDENTIALS file falls through to ADC, not to a leftover
298+
// GOOGLE_CREDENTIALS naming a different service account.
299+
func TestGcpConfigEmptyGACDoesNotFallBackToGoogleCredentials(t *testing.T) {
300+
t.Parallel()
301+
302+
gacFile := filepath.Join(t.TempDir(), "gac.json")
303+
require.NoError(t, os.WriteFile(gacFile, nil, 0o600))
304+
305+
env := map[string]string{
306+
"GOOGLE_APPLICATION_CREDENTIALS": gacFile,
307+
"GOOGLE_CREDENTIALS": string(serviceAccountJSON(t)),
308+
}
309+
310+
clientOpts, err := gcphelper.NewGCPConfigBuilder().
311+
Build(context.Background(), venvtest.NewWithOSFS().WithEnv(env))
312+
require.NoError(t, err)
313+
assert.Empty(t, clientOpts, "leftover GOOGLE_CREDENTIALS must not win over an empty GAC file")
314+
}

internal/tf/cliconfig/user_config.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,18 @@ func LoadUserConfig(v *venv.Venv, opts ...ConfigOption) (*Config, error) {
6868

6969
config := NewConfig(v.FS).WithProviderInstallation(&ProviderInstallation{})
7070

71+
var helperSources []string
72+
7173
for _, path := range paths {
7274
fileConfig, err := loadUserConfigFile(v, path)
7375
if err != nil {
7476
return nil, err
7577
}
7678

79+
if fileConfig.CredentialsHelpers != nil {
80+
helperSources = append(helperSources, path)
81+
}
82+
7783
mergeUserConfig(config, fileConfig)
7884
}
7985

@@ -82,7 +88,7 @@ func LoadUserConfig(v *venv.Venv, opts ...ConfigOption) (*Config, error) {
8288
config.PluginCacheDir = pluginCacheDir
8389
}
8490

85-
if err := validateUserConfig(config); err != nil {
91+
if err := validateUserConfig(config, helperSources); err != nil {
8692
return nil, err
8793
}
8894

@@ -195,6 +201,15 @@ func loadUserConfigFile(v *venv.Venv, path string) (*Config, error) {
195201
return nil, fmt.Errorf("%w: decoding %s: %w", ErrUserConfig, path, err)
196202
}
197203

204+
// Ranging a map to pick "the" helper would decide by iteration order, so reject the
205+
// ambiguity the way OpenTofu's Config.Validate does.
206+
if len(file.CredentialsHelpers) > 1 {
207+
return nil, fmt.Errorf(
208+
"%w: no more than one credentials_helper block may be specified, %s declares %d",
209+
ErrInvalidUserConfig, path, len(file.CredentialsHelpers),
210+
)
211+
}
212+
198213
methods, err := decodeProviderInstallation(path, node)
199214
if err != nil {
200215
return nil, err
@@ -307,8 +322,16 @@ func expandUserConfigEnv(value string, env map[string]string) string {
307322
}
308323

309324
// validateUserConfig rejects the malformed blocks OpenTofu rejects, so a bad hostname surfaces
310-
// here rather than as an unauthenticated registry request later.
311-
func validateUserConfig(config *Config) error {
325+
// here rather than as an unauthenticated registry request later. helperSources names every
326+
// file that declared a credentials_helper, which upstream allows only once across them all.
327+
func validateUserConfig(config *Config, helperSources []string) error {
328+
if len(helperSources) > 1 {
329+
return fmt.Errorf(
330+
"%w: no more than one credentials_helper block may be specified, found one in each of %s",
331+
ErrInvalidUserConfig, strings.Join(helperSources, ", "),
332+
)
333+
}
334+
312335
for _, creds := range config.Credentials {
313336
if _, err := svchost.ForComparison(creds.Name); err != nil {
314337
return fmt.Errorf(
@@ -373,7 +396,8 @@ func userCredentials(file userConfigFile) []ConfigCredentials {
373396
return credentials
374397
}
375398

376-
// userCredentialsHelper returns the single credentials_helper block, or nil when the file declares none.
399+
// userCredentialsHelper returns the single credentials_helper block, or nil when the file
400+
// declares none. More than one is rejected by the caller, so the map holds at most one entry.
377401
func userCredentialsHelper(file userConfigFile) *ConfigCredentialsHelper {
378402
for name, helper := range file.CredentialsHelpers {
379403
var args []string

internal/tf/cliconfig/user_config_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ func TestLoadUserConfig_Errors(t *testing.T) {
297297
source: "provider_installation {\n bogus_mirror {}\n}\n",
298298
expected: cliconfig.ErrInvalidUserConfig,
299299
},
300+
{
301+
name: "two credentials helpers in one file",
302+
source: "credentials_helper \"vault\" {\n args = [\"token\"]\n}\n\ncredentials_helper \"oskeychain\" {\n}\n",
303+
expected: cliconfig.ErrInvalidUserConfig,
304+
},
300305
}
301306

302307
for _, tc := range testCases {
@@ -349,3 +354,28 @@ func userConfigVenv(home string, env map[string]string) *venv.Venv {
349354
WithUserHomeDir(func() (string, error) { return home, nil }).
350355
WithEnv(env)
351356
}
357+
358+
// TestLoadUserConfig_CredentialsHelperAcrossFiles pins that a second helper in a fragment is
359+
// rejected too, rather than silently overriding the one in the main file.
360+
func TestLoadUserConfig_CredentialsHelperAcrossFiles(t *testing.T) {
361+
t.Parallel()
362+
363+
const home = "/virtual/home"
364+
365+
configDir := filepath.Join(home, ".terraform.d")
366+
v := userConfigVenv(home, nil)
367+
368+
require.NoError(t, v.FS.MkdirAll(configDir, 0o755))
369+
require.NoError(t, vfs.WriteFile(v.FS, filepath.Join(home, ".tofurc"), []byte(`
370+
credentials_helper "vault" {
371+
args = ["token"]
372+
}
373+
`), 0o600))
374+
require.NoError(t, vfs.WriteFile(v.FS, filepath.Join(configDir, "10-extra.tfrc"), []byte(`
375+
credentials_helper "oskeychain" {
376+
}
377+
`), 0o600))
378+
379+
_, err := cliconfig.LoadUserConfig(v)
380+
require.ErrorIs(t, err, cliconfig.ErrInvalidUserConfig)
381+
}

0 commit comments

Comments
 (0)