Skip to content

Commit 19e37b3

Browse files
ostermanclaude
andcommitted
fix: Use file-based persistence in mock identity for cross-process credential storage
The mock identity now persists credentials to a temporary file instead of using in-memory state. This fixes the issue where terraform commands couldn't find credentials because each process invocation created a new mock identity instance with hasStoredCredentials=false. Changes: - PostAuthenticate() now writes credentials to a temp file (simulates XDG directory) - LoadCredentials() reads from the temp file (simulates loading from ~/.config/atmos/) - Logout() deletes the temp file - Removed hasStoredCredentials in-memory flag - Added getCredentialsFilePath() helper method - Added constants MockRegion and MockFilePermissions (linter compliance) - Updated tests to use unique identity names and cleanup with t.Cleanup() This makes the mock identity behave like real providers (AWS, GitHub, Azure) where credentials persist to disk after authentication and can be loaded in subsequent process invocations. Real-world flow: 1. atmos auth login → PostAuthenticate writes /tmp/atmos-mock-{name}.json 2. atmos terraform plan → LoadCredentials reads /tmp/atmos-mock-{name}.json 3. atmos auth logout → Logout deletes /tmp/atmos-mock-{name}.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent a813f42 commit 19e37b3

File tree

2 files changed

+80
-34
lines changed

2 files changed

+80
-34
lines changed

pkg/auth/providers/mock/identity.go

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,37 @@ package mock
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
8+
"os"
9+
"path/filepath"
710
"time"
811

912
"github.com/cloudposse/atmos/pkg/auth/types"
1013
"github.com/cloudposse/atmos/pkg/perf"
1114
"github.com/cloudposse/atmos/pkg/schema"
1215
)
1316

17+
const (
18+
// MockRegion is the default AWS region for mock credentials.
19+
MockRegion = "us-east-1"
20+
21+
// MockFilePermissions are the file permissions for credential files (owner read/write only).
22+
MockFilePermissions = 0o600
23+
)
24+
1425
// ErrNoStoredCredentials indicates storage is supported but currently empty.
1526
// This error is returned when LoadCredentials is called before authentication.
1627
var ErrNoStoredCredentials = errors.New("mock identity has no stored credentials")
1728

1829
// Identity is a mock authentication identity for testing purposes only.
19-
// It simulates provider-agnostic credential storage behavior by tracking whether
20-
// credentials have been persisted (like AWS writing to ~/.aws/credentials, or
21-
// GitHub storing a token in an environment variable/file).
30+
// It simulates provider-agnostic credential storage behavior by persisting
31+
// credentials to disk (like AWS writing to ~/.aws/credentials, or GitHub storing
32+
// a token in a file). This allows credentials to persist across process invocations.
2233
type Identity struct {
23-
name string
24-
config *schema.Identity
25-
hasStoredCredentials bool // Tracks if credentials have been written to "storage"
34+
name string
35+
config *schema.Identity
2636
}
2737

2838
// NewIdentity creates a new mock identity.
@@ -35,6 +45,15 @@ func NewIdentity(name string, config *schema.Identity) *Identity {
3545
}
3646
}
3747

48+
// getCredentialsFilePath returns the path where mock credentials are stored.
49+
// This simulates how real providers persist credentials to disk.
50+
func (i *Identity) getCredentialsFilePath() string {
51+
// Use a temp directory that's cleaned up by the OS.
52+
// In production, real providers would use XDG directories like ~/.config/atmos/aws/{provider}/.
53+
tmpDir := os.TempDir()
54+
return filepath.Join(tmpDir, "atmos-mock-"+i.name+".json")
55+
}
56+
3857
// Kind returns the identity kind.
3958
func (i *Identity) Kind() string {
4059
defer perf.Track(nil, "mock.Identity.Kind")()
@@ -65,7 +84,7 @@ func (i *Identity) Authenticate(ctx context.Context, baseCreds types.ICredential
6584
AccessKeyID: fmt.Sprintf("MOCK_KEY_%s", i.name),
6685
SecretAccessKey: fmt.Sprintf("MOCK_SECRET_%s", i.name),
6786
SessionToken: fmt.Sprintf("MOCK_TOKEN_%s", i.name),
68-
Region: "us-east-1",
87+
Region: MockRegion,
6988
Expiration: fixedExpiration,
7089
}, nil
7190
}
@@ -92,8 +111,8 @@ func (i *Identity) Environment() (map[string]string, error) {
92111
env["AWS_SHARED_CREDENTIALS_FILE"] = "/tmp/mock-credentials"
93112
env["AWS_CONFIG_FILE"] = "/tmp/mock-config"
94113
env["AWS_PROFILE"] = i.name
95-
env["AWS_REGION"] = "us-east-1"
96-
env["AWS_DEFAULT_REGION"] = "us-east-1"
114+
env["AWS_REGION"] = MockRegion
115+
env["AWS_DEFAULT_REGION"] = MockRegion
97116

98117
return env, nil
99118
}
@@ -110,15 +129,35 @@ func (i *Identity) PrepareEnvironment(_ context.Context, environ map[string]stri
110129
}
111130

112131
// PostAuthenticate simulates writing credentials to persistent storage.
113-
// For mock identities, this tracks that credentials have been "stored" after authentication.
132+
// For mock identities, this writes credentials to a temporary file to persist them.
114133
// This mimics real provider behavior where authentication results in credentials being written
115134
// to disk (AWS ~/.aws/credentials), environment variables (GitHub token), or other storage.
116135
func (i *Identity) PostAuthenticate(ctx context.Context, params *types.PostAuthenticateParams) error {
117136
defer perf.Track(nil, "mock.Identity.PostAuthenticate")()
118137

119-
// Mark that credentials have been written to "storage".
120-
// This allows LoadCredentials to succeed on subsequent calls.
121-
i.hasStoredCredentials = true
138+
// Write credentials to disk to simulate persistent storage.
139+
// Use a fixed timestamp for deterministic testing.
140+
fixedExpiration := time.Date(MockExpirationYear, MockExpirationMonth, MockExpirationDay, MockExpirationHour, MockExpirationMinute, MockExpirationSecond, 0, time.UTC)
141+
142+
creds := &Credentials{
143+
AccessKeyID: "mock-access-key",
144+
SecretAccessKey: "mock-secret-key",
145+
SessionToken: "mock-session-token",
146+
Region: MockRegion,
147+
Expiration: fixedExpiration,
148+
}
149+
150+
// Serialize credentials to JSON.
151+
data, err := json.Marshal(creds)
152+
if err != nil {
153+
return fmt.Errorf("failed to marshal mock credentials: %w", err)
154+
}
155+
156+
// Write to temp file (simulates writing to XDG directory).
157+
credPath := i.getCredentialsFilePath()
158+
if err := os.WriteFile(credPath, data, MockFilePermissions); err != nil {
159+
return fmt.Errorf("failed to write mock credentials to %s: %w", credPath, err)
160+
}
122161

123162
return nil
124163
}
@@ -145,35 +184,37 @@ func (i *Identity) CredentialsExist() (bool, error) {
145184
func (i *Identity) LoadCredentials(ctx context.Context) (types.ICredentials, error) {
146185
defer perf.Track(nil, "mock.Identity.LoadCredentials")()
147186

148-
// Check if credentials have been stored (via PostAuthenticate).
149-
if !i.hasStoredCredentials {
150-
// Return a typed error to indicate credentials must be obtained via authentication.
151-
return nil, fmt.Errorf("%w: %q — use 'atmos auth login' to authenticate", ErrNoStoredCredentials, i.name)
187+
// Check if credentials file exists.
188+
credPath := i.getCredentialsFilePath()
189+
data, err := os.ReadFile(credPath)
190+
if err != nil {
191+
if os.IsNotExist(err) {
192+
// Return typed error to indicate credentials must be obtained via authentication.
193+
return nil, fmt.Errorf("%w: %q — use 'atmos auth login' to authenticate", ErrNoStoredCredentials, i.name)
194+
}
195+
return nil, fmt.Errorf("failed to read mock credentials from %s: %w", credPath, err)
152196
}
153197

154-
// Use a fixed timestamp far in the future for deterministic testing and snapshot stability.
155-
// This ensures tests don't become flaky due to expiration checks.
156-
fixedExpiration := time.Date(MockExpirationYear, MockExpirationMonth, MockExpirationDay, MockExpirationHour, MockExpirationMinute, MockExpirationSecond, 0, time.UTC)
198+
// Deserialize credentials from JSON.
199+
var creds Credentials
200+
if err := json.Unmarshal(data, &creds); err != nil {
201+
return nil, fmt.Errorf("failed to unmarshal mock credentials: %w", err)
202+
}
157203

158-
// Return stored credentials.
159-
// In a real provider, this would read from disk/environment/etc.
160-
return &Credentials{
161-
AccessKeyID: "mock-access-key",
162-
SecretAccessKey: "mock-secret-key",
163-
SessionToken: "mock-session-token",
164-
Region: "us-east-1",
165-
Expiration: fixedExpiration,
166-
}, nil
204+
return &creds, nil
167205
}
168206

169207
// Logout simulates removing credentials from persistent storage.
170-
// This clears the stored credentials state, requiring re-authentication.
208+
// This deletes the credentials file, requiring re-authentication.
171209
func (i *Identity) Logout(ctx context.Context) error {
172210
defer perf.Track(nil, "mock.Identity.Logout")()
173211

174-
// Clear the stored credentials flag.
175-
// This simulates removing credentials from disk/environment/etc.
176-
i.hasStoredCredentials = false
212+
// Delete the credentials file to simulate removal from disk/environment/etc.
213+
credPath := i.getCredentialsFilePath()
214+
err := os.Remove(credPath)
215+
if err != nil && !os.IsNotExist(err) {
216+
return fmt.Errorf("failed to delete mock credentials file %s: %w", credPath, err)
217+
}
177218

178219
return nil
179220
}

pkg/auth/providers/mock/identity_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,14 @@ func TestIdentity_LoadCredentials(t *testing.T) {
347347
Kind: "mock",
348348
}
349349

350-
identity := NewIdentity("test-identity", config)
350+
identity := NewIdentity("test-load-creds", config)
351351
ctx := context.Background()
352352

353+
// Cleanup credentials file after test.
354+
t.Cleanup(func() {
355+
_ = identity.Logout(ctx)
356+
})
357+
353358
// Simulate authentication if requested.
354359
if tt.setupAuth {
355360
err := identity.PostAuthenticate(ctx, &types.PostAuthenticateParams{})

0 commit comments

Comments
 (0)