Skip to content

Commit 46bc21a

Browse files
authored
Merge branch 'main' into renovate/floci-floci
2 parents cfac414 + c14ce82 commit 46bc21a

24 files changed

Lines changed: 2679 additions & 875 deletions

pkg/auth/cloud/aws/files.go

Lines changed: 209 additions & 279 deletions
Large diffs are not rendered by default.

pkg/auth/cloud/aws/files_test.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ func TestMain(m *testing.M) {
2121
os.Exit(m.Run())
2222
}
2323

24+
// skipIfCannotDenyDirWrite skips tests that rely on removing write permission
25+
// from a directory to force a write failure: the trick is a no-op on Windows
26+
// (permissions work differently) and on Unix when running as root (root
27+
// bypasses permission checks).
28+
func skipIfCannotDenyDirWrite(t *testing.T) {
29+
t.Helper()
30+
if runtime.GOOS == "windows" {
31+
t.Skip("directory write-permission bits are not enforced the same way on Windows")
32+
}
33+
if os.Geteuid() == 0 {
34+
t.Skip("Skipping permission test when running as root")
35+
}
36+
}
37+
2438
func TestAWSFileManager_WriteCredentials(t *testing.T) {
2539
tmp := t.TempDir()
2640
m := &AWSFileManager{baseDir: tmp}
@@ -1030,3 +1044,230 @@ func TestNewAWSFileManager_WithRealm(t *testing.T) {
10301044
})
10311045
}
10321046
}
1047+
1048+
// TestWithFileLock_WrapsErrCacheLocked verifies that withFileLock wraps ErrCacheLocked with the package's ErrFileLockTimeout sentinel and never invokes fn when the lock cannot be acquired.
1049+
// Using a path whose ".lock" sibling lives under a directory that was never created makes the underlying open call fail immediately with ENOENT, since O_CREATE cannot materialize the missing parent directory — no waiting or real timeout involved, on either Unix or Windows.
1050+
func TestWithFileLock_WrapsErrCacheLocked(t *testing.T) {
1051+
if runtime.GOOS == "windows" {
1052+
// Windows uses a best-effort no-op FileLock (see pkg/cache/filelock_windows.go)
1053+
// that never returns ErrCacheLocked, so this branch cannot be exercised there.
1054+
t.Skip("Windows FileLock is a no-op and never reports a lock timeout")
1055+
}
1056+
1057+
path := filepath.Join(t.TempDir(), "missing-subdir", "credentials")
1058+
1059+
fnCalled := false
1060+
err := withFileLock(context.Background(), path, func() error {
1061+
fnCalled = true
1062+
return nil
1063+
})
1064+
1065+
require.Error(t, err)
1066+
assert.ErrorIs(t, err, ErrFileLockTimeout)
1067+
assert.False(t, fnCalled, "fn must not run when the lock cannot be acquired")
1068+
}
1069+
1070+
// TestAWSFileManager_WriteCredentials_WithExpiration verifies that a non-empty
1071+
// Expiration is persisted as the section comment, which is the fallback used
1072+
// to determine credential validity when keychain access is unavailable (e.g.
1073+
// inside Docker containers).
1074+
func TestAWSFileManager_WriteCredentials_WithExpiration(t *testing.T) {
1075+
tmp := t.TempDir()
1076+
m := &AWSFileManager{baseDir: tmp}
1077+
1078+
creds := &types.AWSCredentials{AccessKeyID: "AKIA123", SecretAccessKey: "secret", Expiration: "2099-01-01T00:00:00Z"}
1079+
require.NoError(t, m.WriteCredentials("prov", "dev", creds))
1080+
1081+
cfg, err := ini.Load(m.GetCredentialsPath("prov"))
1082+
require.NoError(t, err)
1083+
sec := cfg.Section("dev")
1084+
// ini re-serializes comments with a leading "; " marker, so assert on the
1085+
// substring rather than exact equality with what was originally set.
1086+
assert.Contains(t, sec.Comment, "atmos: expiration=2099-01-01T00:00:00Z")
1087+
}
1088+
1089+
// TestAWSFileManager_WriteCredentials_LoadFailure verifies that a non-ENOENT
1090+
// failure while loading the existing credentials file (e.g. the path is
1091+
// actually a directory) is surfaced as ErrLoadCredentialsFile and does not
1092+
// fall back to treating it as a missing file.
1093+
func TestAWSFileManager_WriteCredentials_LoadFailure(t *testing.T) {
1094+
tmp := t.TempDir()
1095+
m := &AWSFileManager{baseDir: tmp}
1096+
1097+
credsPath := m.GetCredentialsPath("prov")
1098+
require.NoError(t, os.MkdirAll(credsPath, PermissionRWX)) // Directory in place of the file makes ini.Load fail non-ENOENT.
1099+
1100+
err := m.WriteCredentials("prov", "dev", &types.AWSCredentials{AccessKeyID: "AKIA123", SecretAccessKey: "secret"})
1101+
require.Error(t, err)
1102+
assert.ErrorIs(t, err, ErrLoadCredentialsFile)
1103+
}
1104+
1105+
// TestAWSFileManager_WriteConfig_LoadFailure mirrors the credentials case for
1106+
// WriteConfig's non-ENOENT ini.Load failure branch.
1107+
func TestAWSFileManager_WriteConfig_LoadFailure(t *testing.T) {
1108+
tmp := t.TempDir()
1109+
m := &AWSFileManager{baseDir: tmp}
1110+
1111+
configPath := m.GetConfigPath("prov")
1112+
require.NoError(t, os.MkdirAll(configPath, PermissionRWX))
1113+
1114+
err := m.WriteConfig("prov", "dev", "us-east-1", "json")
1115+
require.Error(t, err)
1116+
assert.ErrorIs(t, err, ErrLoadConfigFile)
1117+
}
1118+
1119+
// TestAWSFileManager_WriteCredentials_SaveFailure verifies that cfg.SaveTo
1120+
// failures (e.g. an unwritable target directory) are surfaced as
1121+
// ErrWriteCredentialsFile.
1122+
func TestAWSFileManager_WriteCredentials_SaveFailure(t *testing.T) {
1123+
skipIfCannotDenyDirWrite(t)
1124+
1125+
tmp := t.TempDir()
1126+
m := &AWSFileManager{baseDir: tmp}
1127+
1128+
credsPath := m.GetCredentialsPath("prov")
1129+
credsDir := filepath.Dir(credsPath)
1130+
require.NoError(t, os.MkdirAll(credsDir, PermissionRWX))
1131+
// Pre-create the sibling lock file so lock acquisition (which also needs to
1132+
// create a file in this directory) still succeeds once the directory is
1133+
// made read-only below; only the SaveTo of the new credentials file itself
1134+
// should be blocked.
1135+
require.NoError(t, os.WriteFile(credsPath+".lock", nil, PermissionRW))
1136+
require.NoError(t, os.Chmod(credsDir, 0o555)) // Read-only: blocks creating the new credentials file.
1137+
t.Cleanup(func() { _ = os.Chmod(credsDir, PermissionRWX) })
1138+
1139+
err := m.WriteCredentials("prov", "dev", &types.AWSCredentials{AccessKeyID: "AKIA123", SecretAccessKey: "secret"})
1140+
require.Error(t, err)
1141+
assert.ErrorIs(t, err, ErrWriteCredentialsFile)
1142+
}
1143+
1144+
// TestAWSFileManager_WriteConfig_SaveFailure mirrors the credentials case for
1145+
// WriteConfig's cfg.SaveTo failure branch.
1146+
func TestAWSFileManager_WriteConfig_SaveFailure(t *testing.T) {
1147+
skipIfCannotDenyDirWrite(t)
1148+
1149+
tmp := t.TempDir()
1150+
m := &AWSFileManager{baseDir: tmp}
1151+
1152+
configPath := m.GetConfigPath("prov")
1153+
configDir := filepath.Dir(configPath)
1154+
require.NoError(t, os.MkdirAll(configDir, PermissionRWX))
1155+
require.NoError(t, os.WriteFile(configPath+".lock", nil, PermissionRW))
1156+
require.NoError(t, os.Chmod(configDir, 0o555))
1157+
t.Cleanup(func() { _ = os.Chmod(configDir, PermissionRWX) })
1158+
1159+
err := m.WriteConfig("prov", "dev", "us-east-1", "json")
1160+
require.Error(t, err)
1161+
assert.ErrorIs(t, err, ErrWriteConfigFile)
1162+
}
1163+
1164+
// TestAWSFileManager_RemoveConfigProfile_LoadFailure verifies the non-ENOENT
1165+
// ini.Load failure branch inside RemoveConfigProfile (distinct from the "file
1166+
// vanished after the existence check" race branch).
1167+
func TestAWSFileManager_RemoveConfigProfile_LoadFailure(t *testing.T) {
1168+
tmp := t.TempDir()
1169+
m := &AWSFileManager{baseDir: tmp}
1170+
1171+
configPath := m.GetConfigPath("prov")
1172+
require.NoError(t, os.MkdirAll(configPath, PermissionRWX)) // Directory in place of the file.
1173+
1174+
err := m.RemoveConfigProfile(context.Background(), "prov", "dev")
1175+
require.Error(t, err)
1176+
assert.ErrorIs(t, err, ErrRemoveProfile)
1177+
}
1178+
1179+
// TestAWSFileManager_RemoveCredentialsProfile_LoadFailure mirrors the config
1180+
// case for RemoveCredentialsProfile's non-ENOENT ini.Load failure branch.
1181+
func TestAWSFileManager_RemoveCredentialsProfile_LoadFailure(t *testing.T) {
1182+
tmp := t.TempDir()
1183+
m := &AWSFileManager{baseDir: tmp}
1184+
1185+
credsPath := m.GetCredentialsPath("prov")
1186+
require.NoError(t, os.MkdirAll(credsPath, PermissionRWX))
1187+
1188+
err := m.RemoveCredentialsProfile(context.Background(), "prov", "dev")
1189+
require.Error(t, err)
1190+
assert.ErrorIs(t, err, ErrRemoveProfile)
1191+
}
1192+
1193+
// TestAWSFileManager_RemoveConfigProfile_SaveFailure verifies that when other
1194+
// profiles remain after the delete, a cfg.SaveTo failure (target file made
1195+
// read-only) is surfaced as ErrRemoveProfile instead of being silently
1196+
// swallowed.
1197+
func TestAWSFileManager_RemoveConfigProfile_SaveFailure(t *testing.T) {
1198+
skipIfCannotDenyDirWrite(t)
1199+
1200+
tmp := t.TempDir()
1201+
m := &AWSFileManager{baseDir: tmp}
1202+
1203+
require.NoError(t, m.WriteConfig("prov", "keep", "us-east-1", "json"))
1204+
require.NoError(t, m.WriteConfig("prov", "dev", "us-west-2", "yaml"))
1205+
1206+
configPath := m.GetConfigPath("prov")
1207+
require.NoError(t, os.Chmod(configPath, 0o444)) // Read-only: blocks the rewrite triggered by deleting "dev".
1208+
t.Cleanup(func() { _ = os.Chmod(configPath, PermissionRW) })
1209+
1210+
err := m.RemoveConfigProfile(context.Background(), "prov", "dev")
1211+
require.Error(t, err)
1212+
assert.ErrorIs(t, err, ErrRemoveProfile)
1213+
}
1214+
1215+
// TestAWSFileManager_RemoveCredentialsProfile_SaveFailure mirrors the config
1216+
// case for RemoveCredentialsProfile's cfg.SaveTo failure branch.
1217+
func TestAWSFileManager_RemoveCredentialsProfile_SaveFailure(t *testing.T) {
1218+
skipIfCannotDenyDirWrite(t)
1219+
1220+
tmp := t.TempDir()
1221+
m := &AWSFileManager{baseDir: tmp}
1222+
1223+
require.NoError(t, m.WriteCredentials("prov", "keep", &types.AWSCredentials{AccessKeyID: "AKIA1", SecretAccessKey: "secret1"}))
1224+
require.NoError(t, m.WriteCredentials("prov", "dev", &types.AWSCredentials{AccessKeyID: "AKIA2", SecretAccessKey: "secret2"}))
1225+
1226+
credsPath := m.GetCredentialsPath("prov")
1227+
require.NoError(t, os.Chmod(credsPath, 0o444))
1228+
t.Cleanup(func() { _ = os.Chmod(credsPath, PermissionRW) })
1229+
1230+
err := m.RemoveCredentialsProfile(context.Background(), "prov", "dev")
1231+
require.Error(t, err)
1232+
assert.ErrorIs(t, err, ErrRemoveProfile)
1233+
}
1234+
1235+
// TestAWSFileManager_RemoveConfigProfile_RemoveFailure verifies that when no
1236+
// profiles remain and the file must be deleted, an os.Remove failure (the
1237+
// containing directory made read-only, which blocks unlink on POSIX even
1238+
// though the file itself is readable) is surfaced as ErrRemoveProfile.
1239+
func TestAWSFileManager_RemoveConfigProfile_RemoveFailure(t *testing.T) {
1240+
skipIfCannotDenyDirWrite(t)
1241+
1242+
tmp := t.TempDir()
1243+
m := &AWSFileManager{baseDir: tmp}
1244+
1245+
require.NoError(t, m.WriteConfig("prov", "dev", "us-east-1", "json"))
1246+
1247+
configDir := filepath.Dir(m.GetConfigPath("prov"))
1248+
require.NoError(t, os.Chmod(configDir, 0o555)) // Read-only: blocks unlinking the now-empty config file.
1249+
t.Cleanup(func() { _ = os.Chmod(configDir, PermissionRWX) })
1250+
1251+
err := m.RemoveConfigProfile(context.Background(), "prov", "dev")
1252+
require.Error(t, err)
1253+
assert.ErrorIs(t, err, ErrRemoveProfile)
1254+
}
1255+
1256+
// TestAWSFileManager_RemoveCredentialsProfile_RemoveFailure mirrors the
1257+
// config case for RemoveCredentialsProfile's os.Remove failure branch.
1258+
func TestAWSFileManager_RemoveCredentialsProfile_RemoveFailure(t *testing.T) {
1259+
skipIfCannotDenyDirWrite(t)
1260+
1261+
tmp := t.TempDir()
1262+
m := &AWSFileManager{baseDir: tmp}
1263+
1264+
require.NoError(t, m.WriteCredentials("prov", "dev", &types.AWSCredentials{AccessKeyID: "AKIA123", SecretAccessKey: "secret"}))
1265+
1266+
credsDir := filepath.Dir(m.GetCredentialsPath("prov"))
1267+
require.NoError(t, os.Chmod(credsDir, 0o555))
1268+
t.Cleanup(func() { _ = os.Chmod(credsDir, PermissionRWX) })
1269+
1270+
err := m.RemoveCredentialsProfile(context.Background(), "prov", "dev")
1271+
require.Error(t, err)
1272+
assert.ErrorIs(t, err, ErrRemoveProfile)
1273+
}

0 commit comments

Comments
 (0)