Replace symlinks with plain files containing relative paths
- Remove all symlink creation and resolution in favor of plain files - currentvault file now contains relative path like "vaults.d/default" - current-unlocker file now contains relative path like "unlockers.d/passphrase" - current version file now contains relative path like "versions/20231215.001" - Simplify path resolution to just read file contents and join with parent dir - Update all tests to read files instead of using os.Readlink
This commit is contained in:
parent
18fb79e971
commit
949a5aee61
@ -332,16 +332,12 @@ func test01Initialize(t *testing.T, tempDir, testMnemonic, testPassphrase string
|
|||||||
defaultVaultDir := filepath.Join(vaultsDir, "default")
|
defaultVaultDir := filepath.Join(vaultsDir, "default")
|
||||||
verifyFileExists(t, defaultVaultDir)
|
verifyFileExists(t, defaultVaultDir)
|
||||||
|
|
||||||
// Check currentvault symlink - it may be absolute or relative
|
// Check currentvault file contains the relative path
|
||||||
currentVaultLink := filepath.Join(tempDir, "currentvault")
|
currentVaultFile := filepath.Join(tempDir, "currentvault")
|
||||||
target, err := os.Readlink(currentVaultLink)
|
targetBytes, err := os.ReadFile(currentVaultFile)
|
||||||
require.NoError(t, err, "should be able to read currentvault symlink")
|
require.NoError(t, err, "should be able to read currentvault file")
|
||||||
// Check if it points to the right place (handle both absolute and relative)
|
target := string(targetBytes)
|
||||||
if filepath.IsAbs(target) {
|
assert.Equal(t, "vaults.d/default", target, "currentvault should contain relative path")
|
||||||
assert.Equal(t, filepath.Join(tempDir, "vaults.d/default"), target)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, "vaults.d/default", target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify vault structure
|
// Verify vault structure
|
||||||
pubKeyFile := filepath.Join(defaultVaultDir, "pub.age")
|
pubKeyFile := filepath.Join(defaultVaultDir, "pub.age")
|
||||||
@ -366,22 +362,12 @@ func test01Initialize(t *testing.T, tempDir, testMnemonic, testPassphrase string
|
|||||||
encryptedLTPubKey := filepath.Join(passphraseUnlockerDir, "pub.age")
|
encryptedLTPubKey := filepath.Join(passphraseUnlockerDir, "pub.age")
|
||||||
verifyFileExists(t, encryptedLTPubKey)
|
verifyFileExists(t, encryptedLTPubKey)
|
||||||
|
|
||||||
// Check current-unlocker file
|
// Check current-unlocker file contains the relative path
|
||||||
currentUnlockerFile := filepath.Join(defaultVaultDir, "current-unlocker")
|
currentUnlockerFile := filepath.Join(defaultVaultDir, "current-unlocker")
|
||||||
verifyFileExists(t, currentUnlockerFile)
|
verifyFileExists(t, currentUnlockerFile)
|
||||||
|
|
||||||
// Read the current-unlocker symlink to see what it points to
|
|
||||||
symlinkTarget, err := os.Readlink(currentUnlockerFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("DEBUG: failed to read symlink %s: %v", currentUnlockerFile, err)
|
|
||||||
// Fallback to reading as file if it's not a symlink
|
|
||||||
currentUnlockerContent := readFile(t, currentUnlockerFile)
|
currentUnlockerContent := readFile(t, currentUnlockerFile)
|
||||||
t.Logf("DEBUG: current-unlocker file content: %q", string(currentUnlockerContent))
|
assert.Contains(t, string(currentUnlockerContent), "passphrase", "current unlocker should point to passphrase type")
|
||||||
assert.Contains(t, string(currentUnlockerContent), "passphrase", "current unlocker should be passphrase type")
|
|
||||||
} else {
|
|
||||||
t.Logf("DEBUG: current-unlocker symlink points to: %q", symlinkTarget)
|
|
||||||
assert.Contains(t, symlinkTarget, "passphrase", "current unlocker should be passphrase type")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify vault-metadata.json in vault
|
// Verify vault-metadata.json in vault
|
||||||
vaultMetadata := filepath.Join(defaultVaultDir, "vault-metadata.json")
|
vaultMetadata := filepath.Join(defaultVaultDir, "vault-metadata.json")
|
||||||
@ -472,17 +458,12 @@ func test03CreateVault(t *testing.T, tempDir string, runSecret func(...string) (
|
|||||||
workVaultDir := filepath.Join(tempDir, "vaults.d", "work")
|
workVaultDir := filepath.Join(tempDir, "vaults.d", "work")
|
||||||
verifyFileExists(t, workVaultDir)
|
verifyFileExists(t, workVaultDir)
|
||||||
|
|
||||||
// Check currentvault symlink was updated
|
// Check currentvault file was updated
|
||||||
currentVaultLink := filepath.Join(tempDir, "currentvault")
|
currentVaultFile := filepath.Join(tempDir, "currentvault")
|
||||||
target, err := os.Readlink(currentVaultLink)
|
targetBytes, err := os.ReadFile(currentVaultFile)
|
||||||
require.NoError(t, err, "should be able to read currentvault symlink")
|
require.NoError(t, err, "should be able to read currentvault file")
|
||||||
|
target := string(targetBytes)
|
||||||
// The symlink should now point to work vault
|
assert.Equal(t, "vaults.d/work", target, "currentvault should contain relative path to work")
|
||||||
if filepath.IsAbs(target) {
|
|
||||||
assert.Equal(t, filepath.Join(tempDir, "vaults.d/work"), target)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, "vaults.d/work", target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify work vault has basic structure
|
// Verify work vault has basic structure
|
||||||
unlockersDir := filepath.Join(workVaultDir, "unlockers.d")
|
unlockersDir := filepath.Join(workVaultDir, "unlockers.d")
|
||||||
@ -608,15 +589,16 @@ func test05AddSecret(t *testing.T, tempDir, testMnemonic string, runSecret func(
|
|||||||
metadataFile := filepath.Join(versionDir, "metadata.age")
|
metadataFile := filepath.Join(versionDir, "metadata.age")
|
||||||
verifyFileExists(t, metadataFile)
|
verifyFileExists(t, metadataFile)
|
||||||
|
|
||||||
// Check current symlink
|
// Check current file
|
||||||
currentLink := filepath.Join(secretDir, "current")
|
currentLink := filepath.Join(secretDir, "current")
|
||||||
verifyFileExists(t, currentLink)
|
verifyFileExists(t, currentLink)
|
||||||
|
|
||||||
// Verify symlink points to the version directory
|
// Verify current file contains the version path
|
||||||
target, err := os.Readlink(currentLink)
|
targetBytes, err := os.ReadFile(currentLink)
|
||||||
require.NoError(t, err, "should read current symlink")
|
require.NoError(t, err, "should read current file")
|
||||||
|
target := string(targetBytes)
|
||||||
expectedTarget := filepath.Join("versions", versionName)
|
expectedTarget := filepath.Join("versions", versionName)
|
||||||
assert.Equal(t, expectedTarget, target, "current symlink should point to version")
|
assert.Equal(t, expectedTarget, target, "current file should point to version")
|
||||||
|
|
||||||
// Verify we can retrieve the secret
|
// Verify we can retrieve the secret
|
||||||
getOutput, err := runSecretWithEnv(map[string]string{
|
getOutput, err := runSecretWithEnv(map[string]string{
|
||||||
@ -698,12 +680,13 @@ func test07AddSecretVersion(t *testing.T, tempDir, testMnemonic string, runSecre
|
|||||||
verifyFileExists(t, filepath.Join(versionDir, "metadata.age"))
|
verifyFileExists(t, filepath.Join(versionDir, "metadata.age"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check current symlink points to new version
|
// Check current file points to new version
|
||||||
currentLink := filepath.Join(secretDir, "current")
|
currentLink := filepath.Join(secretDir, "current")
|
||||||
target, err := os.Readlink(currentLink)
|
targetBytes, err := os.ReadFile(currentLink)
|
||||||
require.NoError(t, err, "should read current symlink")
|
require.NoError(t, err, "should read current file")
|
||||||
|
target := string(targetBytes)
|
||||||
expectedTarget := filepath.Join("versions", newVersion)
|
expectedTarget := filepath.Join("versions", newVersion)
|
||||||
assert.Equal(t, expectedTarget, target, "current symlink should point to new version")
|
assert.Equal(t, expectedTarget, target, "current file should point to new version")
|
||||||
|
|
||||||
// Verify we get the new value when retrieving the secret
|
// Verify we get the new value when retrieving the secret
|
||||||
getOutput, err := runSecretWithEnv(map[string]string{
|
getOutput, err := runSecretWithEnv(map[string]string{
|
||||||
@ -815,8 +798,9 @@ func test10PromoteVersion(t *testing.T, tempDir, testMnemonic string, runSecret
|
|||||||
|
|
||||||
// Before promotion, current should point to .002 (from test 07)
|
// Before promotion, current should point to .002 (from test 07)
|
||||||
currentLink := filepath.Join(defaultVaultDir, "secrets.d", "database%password", "current")
|
currentLink := filepath.Join(defaultVaultDir, "secrets.d", "database%password", "current")
|
||||||
target, err := os.Readlink(currentLink)
|
targetBytes, err := os.ReadFile(currentLink)
|
||||||
require.NoError(t, err, "should read current symlink")
|
require.NoError(t, err, "should read current file")
|
||||||
|
target := string(targetBytes)
|
||||||
assert.Equal(t, filepath.Join("versions", version002), target, "current should initially point to .002")
|
assert.Equal(t, filepath.Join("versions", version002), target, "current should initially point to .002")
|
||||||
|
|
||||||
// Promote the old version
|
// Promote the old version
|
||||||
@ -828,11 +812,12 @@ func test10PromoteVersion(t *testing.T, tempDir, testMnemonic string, runSecret
|
|||||||
assert.Contains(t, output, "Promoted version", "should confirm promotion")
|
assert.Contains(t, output, "Promoted version", "should confirm promotion")
|
||||||
assert.Contains(t, output, version001, "should mention the promoted version")
|
assert.Contains(t, output, version001, "should mention the promoted version")
|
||||||
|
|
||||||
// Verify symlink was updated
|
// Verify current file was updated
|
||||||
newTarget, err := os.Readlink(currentLink)
|
newTargetBytes, err := os.ReadFile(currentLink)
|
||||||
require.NoError(t, err, "should read current symlink after promotion")
|
require.NoError(t, err, "should read current file after promotion")
|
||||||
|
newTarget := string(newTargetBytes)
|
||||||
expectedTarget := filepath.Join("versions", version001)
|
expectedTarget := filepath.Join("versions", version001)
|
||||||
assert.Equal(t, expectedTarget, newTarget, "current symlink should now point to .001")
|
assert.Equal(t, expectedTarget, newTarget, "current file should now point to .001")
|
||||||
|
|
||||||
// Verify we now get the old value when retrieving the secret
|
// Verify we now get the old value when retrieving the secret
|
||||||
getOutput, err := runSecretWithEnv(map[string]string{
|
getOutput, err := runSecretWithEnv(map[string]string{
|
||||||
@ -1251,27 +1236,21 @@ func test14SwitchVault(t *testing.T, tempDir string, runSecret func(...string) (
|
|||||||
require.NoError(t, err, "vault select default should succeed")
|
require.NoError(t, err, "vault select default should succeed")
|
||||||
|
|
||||||
// Verify current vault is default
|
// Verify current vault is default
|
||||||
currentVaultLink := filepath.Join(tempDir, "currentvault")
|
currentVaultFile := filepath.Join(tempDir, "currentvault")
|
||||||
target, err := os.Readlink(currentVaultLink)
|
targetBytes, err := os.ReadFile(currentVaultFile)
|
||||||
require.NoError(t, err, "should read currentvault symlink")
|
require.NoError(t, err, "should read currentvault file")
|
||||||
if filepath.IsAbs(target) {
|
target := string(targetBytes)
|
||||||
assert.Contains(t, target, "vaults.d/default")
|
assert.Equal(t, "vaults.d/default", target, "currentvault should point to default")
|
||||||
} else {
|
|
||||||
assert.Contains(t, target, "default")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to work vault
|
// Switch to work vault
|
||||||
_, err = runSecret("vault", "select", "work")
|
_, err = runSecret("vault", "select", "work")
|
||||||
require.NoError(t, err, "vault select work should succeed")
|
require.NoError(t, err, "vault select work should succeed")
|
||||||
|
|
||||||
// Verify current vault is now work
|
// Verify current vault is now work
|
||||||
target, err = os.Readlink(currentVaultLink)
|
targetBytes, err = os.ReadFile(currentVaultFile)
|
||||||
require.NoError(t, err, "should read currentvault symlink")
|
require.NoError(t, err, "should read currentvault file")
|
||||||
if filepath.IsAbs(target) {
|
target = string(targetBytes)
|
||||||
assert.Contains(t, target, "vaults.d/work")
|
assert.Equal(t, "vaults.d/work", target, "currentvault should point to work")
|
||||||
} else {
|
|
||||||
assert.Contains(t, target, "work")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch back to default
|
// Switch back to default
|
||||||
_, err = runSecret("vault", "select", "default")
|
_, err = runSecret("vault", "select", "default")
|
||||||
@ -2006,26 +1985,28 @@ func test28VaultMetadata(t *testing.T, tempDir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func test29SymlinkHandling(t *testing.T, tempDir, secretPath, testMnemonic string) {
|
func test29SymlinkHandling(t *testing.T, tempDir, secretPath, testMnemonic string) {
|
||||||
// Test currentvault symlink
|
// Test currentvault file
|
||||||
currentVaultLink := filepath.Join(tempDir, "currentvault")
|
currentVaultFile := filepath.Join(tempDir, "currentvault")
|
||||||
verifyFileExists(t, currentVaultLink)
|
verifyFileExists(t, currentVaultFile)
|
||||||
|
|
||||||
// Read the symlink
|
// Read the file
|
||||||
target, err := os.Readlink(currentVaultLink)
|
targetBytes, err := os.ReadFile(currentVaultFile)
|
||||||
require.NoError(t, err, "should read currentvault symlink")
|
require.NoError(t, err, "should read currentvault file")
|
||||||
|
target := string(targetBytes)
|
||||||
assert.Contains(t, target, "vaults.d", "should point to vaults.d directory")
|
assert.Contains(t, target, "vaults.d", "should point to vaults.d directory")
|
||||||
|
|
||||||
// Test version current symlink
|
// Test version current file
|
||||||
defaultVaultDir := filepath.Join(tempDir, "vaults.d", "default")
|
defaultVaultDir := filepath.Join(tempDir, "vaults.d", "default")
|
||||||
secretDir := filepath.Join(defaultVaultDir, "secrets.d", "database%password")
|
secretDir := filepath.Join(defaultVaultDir, "secrets.d", "database%password")
|
||||||
currentLink := filepath.Join(secretDir, "current")
|
currentLink := filepath.Join(secretDir, "current")
|
||||||
|
|
||||||
verifyFileExists(t, currentLink)
|
verifyFileExists(t, currentLink)
|
||||||
target, err = os.Readlink(currentLink)
|
targetBytes, err = os.ReadFile(currentLink)
|
||||||
require.NoError(t, err, "should read current version symlink")
|
require.NoError(t, err, "should read current version file")
|
||||||
|
target = string(targetBytes)
|
||||||
assert.Contains(t, target, "versions", "should point to versions directory")
|
assert.Contains(t, target, "versions", "should point to versions directory")
|
||||||
|
|
||||||
// Test that symlinks update properly
|
// Test that current file updates properly
|
||||||
// Add new version
|
// Add new version
|
||||||
cmd := exec.Command(secretPath, "add", "database/password", "--force")
|
cmd := exec.Command(secretPath, "add", "database/password", "--force")
|
||||||
cmd.Env = []string{
|
cmd.Env = []string{
|
||||||
@ -2038,11 +2019,12 @@ func test29SymlinkHandling(t *testing.T, tempDir, secretPath, testMnemonic strin
|
|||||||
_, err = cmd.CombinedOutput()
|
_, err = cmd.CombinedOutput()
|
||||||
require.NoError(t, err, "add new version should succeed")
|
require.NoError(t, err, "add new version should succeed")
|
||||||
|
|
||||||
// Check that symlink was updated
|
// Check that current file was updated
|
||||||
newTarget, err := os.Readlink(currentLink)
|
newTargetBytes, err := os.ReadFile(currentLink)
|
||||||
require.NoError(t, err, "should read updated symlink")
|
require.NoError(t, err, "should read updated current file")
|
||||||
assert.NotEqual(t, target, newTarget, "symlink should point to new version")
|
newTarget := string(newTargetBytes)
|
||||||
assert.Contains(t, newTarget, "versions", "new symlink should still point to versions directory")
|
assert.NotEqual(t, target, newTarget, "current file should point to new version")
|
||||||
|
assert.Contains(t, newTarget, "versions", "new current file should still point to versions directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||||
@ -2078,18 +2060,11 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string,
|
|||||||
err = copyDir(filepath.Join(tempDir, "vaults.d"), filepath.Join(backupDir, "vaults.d"))
|
err = copyDir(filepath.Join(tempDir, "vaults.d"), filepath.Join(backupDir, "vaults.d"))
|
||||||
require.NoError(t, err, "backup vaults should succeed")
|
require.NoError(t, err, "backup vaults should succeed")
|
||||||
|
|
||||||
// Also backup the currentvault symlink/file
|
// Also backup the currentvault file
|
||||||
currentVaultSrc := filepath.Join(tempDir, "currentvault")
|
currentVaultSrc := filepath.Join(tempDir, "currentvault")
|
||||||
currentVaultDst := filepath.Join(backupDir, "currentvault")
|
currentVaultDst := filepath.Join(backupDir, "currentvault")
|
||||||
if target, err := os.Readlink(currentVaultSrc); err == nil {
|
|
||||||
// It's a symlink, recreate it
|
|
||||||
err = os.Symlink(target, currentVaultDst)
|
|
||||||
require.NoError(t, err, "backup currentvault symlink should succeed")
|
|
||||||
} else {
|
|
||||||
// It's a regular file, copy it
|
|
||||||
data := readFile(t, currentVaultSrc)
|
data := readFile(t, currentVaultSrc)
|
||||||
writeFile(t, currentVaultDst, data)
|
writeFile(t, currentVaultDst, data)
|
||||||
}
|
|
||||||
|
|
||||||
// Add more secrets after backup
|
// Add more secrets after backup
|
||||||
cmd := exec.Command(secretPath, "add", "post-backup/secret", "--force")
|
cmd := exec.Command(secretPath, "add", "post-backup/secret", "--force")
|
||||||
@ -2119,13 +2094,8 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string,
|
|||||||
|
|
||||||
// Restore currentvault
|
// Restore currentvault
|
||||||
os.Remove(currentVaultSrc)
|
os.Remove(currentVaultSrc)
|
||||||
if target, err := os.Readlink(currentVaultDst); err == nil {
|
restoredData := readFile(t, currentVaultDst)
|
||||||
err = os.Symlink(target, currentVaultSrc)
|
writeFile(t, currentVaultSrc, restoredData)
|
||||||
require.NoError(t, err, "restore currentvault symlink should succeed")
|
|
||||||
} else {
|
|
||||||
data := readFile(t, currentVaultDst)
|
|
||||||
writeFile(t, currentVaultSrc, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify original secrets are restored
|
// Verify original secrets are restored
|
||||||
output, err = runSecretWithEnv(map[string]string{
|
output, err = runSecretWithEnv(map[string]string{
|
||||||
@ -2267,18 +2237,7 @@ func copyDir(src, dst string) error {
|
|||||||
srcPath := filepath.Join(src, entry.Name())
|
srcPath := filepath.Join(src, entry.Name())
|
||||||
dstPath := filepath.Join(dst, entry.Name())
|
dstPath := filepath.Join(dst, entry.Name())
|
||||||
|
|
||||||
// Check if it's a symlink
|
if entry.IsDir() {
|
||||||
if info, err := os.Lstat(srcPath); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
// It's a symlink - read and recreate it
|
|
||||||
target, err := os.Readlink(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.Symlink(target, dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if entry.IsDir() {
|
|
||||||
err = copyDir(srcPath, dstPath)
|
err = copyDir(srcPath, dstPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -431,59 +430,37 @@ func ListVersions(fs afero.Fs, secretDir string) ([]string, error) {
|
|||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentVersion returns the version that the "current" symlink points to
|
// GetCurrentVersion returns the version that the "current" file points to
|
||||||
func GetCurrentVersion(fs afero.Fs, secretDir string) (string, error) {
|
func GetCurrentVersion(fs afero.Fs, secretDir string) (string, error) {
|
||||||
currentPath := filepath.Join(secretDir, "current")
|
currentPath := filepath.Join(secretDir, "current")
|
||||||
|
|
||||||
// Try to read as a real symlink first
|
fileData, err := afero.ReadFile(fs, currentPath)
|
||||||
if _, ok := fs.(*afero.OsFs); ok {
|
if err != nil {
|
||||||
target, err := os.Readlink(currentPath)
|
return "", fmt.Errorf("failed to read current version file: %w", err)
|
||||||
if err == nil {
|
}
|
||||||
|
|
||||||
|
target := strings.TrimSpace(string(fileData))
|
||||||
|
|
||||||
// Extract version from path (e.g., "versions/20231215.001" -> "20231215.001")
|
// Extract version from path (e.g., "versions/20231215.001" -> "20231215.001")
|
||||||
parts := strings.Split(target, "/")
|
parts := strings.Split(target, "/")
|
||||||
if len(parts) >= 2 && parts[0] == "versions" {
|
if len(parts) >= 2 && parts[0] == "versions" {
|
||||||
return parts[1], nil
|
return parts[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("invalid current version symlink format: %s", target)
|
return "", fmt.Errorf("invalid current version file format: %s", target)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to reading as a file (for MemMapFs testing)
|
|
||||||
fileData, err := afero.ReadFile(fs, currentPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to read current version symlink: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
target := strings.TrimSpace(string(fileData))
|
|
||||||
|
|
||||||
// Extract version from path
|
|
||||||
parts := strings.Split(target, "/")
|
|
||||||
if len(parts) >= 2 && parts[0] == "versions" {
|
|
||||||
return parts[1], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("invalid current version symlink format: %s", target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCurrentVersion updates the "current" symlink to point to a specific version
|
// SetCurrentVersion updates the "current" file to point to a specific version
|
||||||
func SetCurrentVersion(fs afero.Fs, secretDir string, version string) error {
|
func SetCurrentVersion(fs afero.Fs, secretDir string, version string) error {
|
||||||
currentPath := filepath.Join(secretDir, "current")
|
currentPath := filepath.Join(secretDir, "current")
|
||||||
targetPath := filepath.Join("versions", version)
|
targetPath := filepath.Join("versions", version)
|
||||||
|
|
||||||
// Remove existing symlink if it exists
|
// Remove existing file if it exists
|
||||||
_ = fs.Remove(currentPath)
|
_ = fs.Remove(currentPath)
|
||||||
|
|
||||||
// Try to create a real symlink first (works on Unix systems)
|
// Write the relative path to the file
|
||||||
if _, ok := fs.(*afero.OsFs); ok {
|
|
||||||
if err := os.Symlink(targetPath, currentPath); err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to creating a file with the target path (for MemMapFs testing)
|
|
||||||
if err := afero.WriteFile(fs, currentPath, []byte(targetPath), FilePerms); err != nil {
|
if err := afero.WriteFile(fs, currentPath, []byte(targetPath), FilePerms); err != nil {
|
||||||
return fmt.Errorf("failed to create current version symlink: %w", err)
|
return fmt.Errorf("failed to create current version file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -26,9 +26,9 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
|||||||
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
||||||
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
|
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
|
||||||
|
|
||||||
// Test symlink handling
|
// Test currentvault file handling (plain file with relative path)
|
||||||
t.Run("SymlinkHandling", func(t *testing.T) {
|
t.Run("CurrentVaultFileHandling", func(t *testing.T) {
|
||||||
stateDir := filepath.Join(tempDir, "symlink-test")
|
stateDir := filepath.Join(tempDir, "currentvault-test")
|
||||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||||
t.Fatalf("Failed to create state dir: %v", err)
|
t.Fatalf("Failed to create state dir: %v", err)
|
||||||
}
|
}
|
||||||
@ -45,31 +45,26 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
|||||||
t.Fatalf("Failed to get vault directory: %v", err)
|
t.Fatalf("Failed to get vault directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a symlink to the vault directory in a different location
|
// Verify the currentvault file exists and contains the right relative path
|
||||||
symlinkPath := filepath.Join(tempDir, "test-symlink")
|
currentVaultPath := filepath.Join(stateDir, "currentvault")
|
||||||
if err := os.Symlink(vaultDir, symlinkPath); err != nil {
|
currentVaultContents, err := os.ReadFile(currentVaultPath)
|
||||||
t.Fatalf("Failed to create symlink: %v", err)
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read currentvault file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that we can resolve the symlink correctly
|
expectedRelativePath := "vaults.d/test-vault"
|
||||||
resolvedPath, err := vault.ResolveVaultSymlink(fs, symlinkPath)
|
if string(currentVaultContents) != expectedRelativePath {
|
||||||
if err != nil {
|
t.Errorf("Expected currentvault to contain %q, got %q", expectedRelativePath, string(currentVaultContents))
|
||||||
t.Fatalf("Failed to resolve symlink: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// On some platforms, the resolved path might have different case or format
|
// Test that ResolveVaultSymlink correctly resolves the path
|
||||||
// We'll use filepath.EvalSymlinks to get the canonical path for comparison
|
resolvedPath, err := vault.ResolveVaultSymlink(fs, currentVaultPath)
|
||||||
expectedPath, err := filepath.EvalSymlinks(vaultDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to evaluate symlink: %v", err)
|
t.Fatalf("Failed to resolve currentvault path: %v", err)
|
||||||
}
|
|
||||||
actualPath, err := filepath.EvalSymlinks(resolvedPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to evaluate resolved path: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if actualPath != expectedPath {
|
if resolvedPath != vaultDir {
|
||||||
t.Errorf("Expected symlink to resolve to %s, got %s", expectedPath, actualPath)
|
t.Errorf("Expected resolved path to be %s, got %s", vaultDir, resolvedPath)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
"git.eeqj.de/sneak/secret/internal/secret"
|
||||||
@ -31,104 +32,31 @@ func isValidVaultName(name string) bool {
|
|||||||
return matched
|
return matched
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveRelativeSymlink resolves a relative symlink target to an absolute path
|
// ResolveVaultSymlink reads the currentvault file to get the path to the current vault
|
||||||
func resolveRelativeSymlink(symlinkPath, _ string) (string, error) {
|
// The file contains a relative path to the vault directory
|
||||||
// Get the current directory before changing
|
func ResolveVaultSymlink(fs afero.Fs, currentVaultPath string) (string, error) {
|
||||||
originalDir, err := os.Getwd()
|
secret.Debug("resolveVaultSymlink starting", "path", currentVaultPath)
|
||||||
|
|
||||||
|
fileData, err := afero.ReadFile(fs, currentVaultPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get current directory: %w", err)
|
secret.Debug("Failed to read currentvault file", "error", err)
|
||||||
}
|
|
||||||
secret.Debug("Got current directory", "original_dir", originalDir)
|
|
||||||
|
|
||||||
// Change to the symlink's directory
|
return "", fmt.Errorf("failed to read currentvault file: %w", err)
|
||||||
symlinkDir := filepath.Dir(symlinkPath)
|
|
||||||
secret.Debug("Changing to symlink directory", "symlink_path", symlinkDir)
|
|
||||||
secret.Debug("About to call os.Chdir - this might hang if symlink is broken")
|
|
||||||
if err := os.Chdir(symlinkDir); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to change to symlink directory: %w", err)
|
|
||||||
}
|
}
|
||||||
secret.Debug("Changed to symlink directory successfully - os.Chdir completed")
|
|
||||||
|
|
||||||
// Get the absolute path of the target
|
// The file contains a relative path like "vaults.d/default"
|
||||||
secret.Debug("Getting absolute path of current directory")
|
relativePath := strings.TrimSpace(string(fileData))
|
||||||
absolutePath, err := os.Getwd()
|
secret.Debug("Read relative path from file", "relative_path", relativePath)
|
||||||
if err != nil {
|
|
||||||
// Try to restore original directory before returning error
|
|
||||||
_ = os.Chdir(originalDir)
|
|
||||||
|
|
||||||
return "", fmt.Errorf("failed to get absolute path: %w", err)
|
// Resolve to absolute path relative to the state directory
|
||||||
}
|
stateDir := filepath.Dir(currentVaultPath)
|
||||||
secret.Debug("Got absolute path", "absolute_path", absolutePath)
|
absolutePath := filepath.Join(stateDir, relativePath)
|
||||||
|
|
||||||
// Restore the original directory
|
secret.Debug("Resolved to absolute path", "absolute_path", absolutePath)
|
||||||
secret.Debug("Restoring original directory", "original_dir", originalDir)
|
|
||||||
if err := os.Chdir(originalDir); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to restore original directory: %w", err)
|
|
||||||
}
|
|
||||||
secret.Debug("Restored original directory successfully")
|
|
||||||
|
|
||||||
return absolutePath, nil
|
return absolutePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveVaultSymlink resolves the currentvault symlink by reading either the symlink target or file contents
|
|
||||||
// This function is designed to work on both Unix and Windows systems, as well as with in-memory filesystems
|
|
||||||
func ResolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
|
|
||||||
secret.Debug("resolveVaultSymlink starting", "symlink_path", symlinkPath)
|
|
||||||
|
|
||||||
// First try to handle the path as a real symlink (works on Unix systems)
|
|
||||||
_, isOsFs := fs.(*afero.OsFs)
|
|
||||||
if isOsFs {
|
|
||||||
target, err := tryResolveOsSymlink(symlinkPath)
|
|
||||||
if err == nil {
|
|
||||||
secret.Debug("resolveVaultSymlink completed successfully", "result", target)
|
|
||||||
|
|
||||||
return target, nil
|
|
||||||
}
|
|
||||||
// Fall through to fallback if symlink resolution failed
|
|
||||||
} else {
|
|
||||||
secret.Debug("Not using OS filesystem, skipping symlink resolution")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: treat it as a regular file containing the target path
|
|
||||||
secret.Debug("Fallback: trying to read regular file with target path")
|
|
||||||
|
|
||||||
fileData, err := afero.ReadFile(fs, symlinkPath)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to read target path file", "error", err)
|
|
||||||
|
|
||||||
return "", fmt.Errorf("failed to read vault symlink: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
target := string(fileData)
|
|
||||||
secret.Debug("Read target path from file", "target", target)
|
|
||||||
|
|
||||||
secret.Debug("resolveVaultSymlink completed via fallback", "result", target)
|
|
||||||
|
|
||||||
return target, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryResolveOsSymlink attempts to resolve a symlink on OS filesystems
|
|
||||||
func tryResolveOsSymlink(symlinkPath string) (string, error) {
|
|
||||||
secret.Debug("Using real filesystem symlink resolution")
|
|
||||||
|
|
||||||
// Check if the symlink exists
|
|
||||||
secret.Debug("Checking symlink target", "symlink_path", symlinkPath)
|
|
||||||
target, err := os.Readlink(symlinkPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
secret.Debug("Symlink points to", "target", target)
|
|
||||||
|
|
||||||
// On real filesystem, we need to handle relative symlinks
|
|
||||||
// by resolving them relative to the symlink's directory
|
|
||||||
if !filepath.IsAbs(target) {
|
|
||||||
return resolveRelativeSymlink(symlinkPath, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
return target, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentVault gets the current vault from the file system
|
// GetCurrentVault gets the current vault from the file system
|
||||||
func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
|
func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
|
||||||
secret.Debug("Getting current vault", "state_dir", stateDir)
|
secret.Debug("Getting current vault", "state_dir", stateDir)
|
||||||
@ -328,32 +256,19 @@ func SelectVault(fs afero.Fs, stateDir string, name string) error {
|
|||||||
return fmt.Errorf("vault %s does not exist", name)
|
return fmt.Errorf("vault %s does not exist", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update the current vault symlink/file
|
// Create or update the currentvault file with the relative path
|
||||||
currentVaultPath := filepath.Join(stateDir, "currentvault")
|
currentVaultPath := filepath.Join(stateDir, "currentvault")
|
||||||
targetPath := filepath.Join(stateDir, "vaults.d", name)
|
relativePath := filepath.Join("vaults.d", name)
|
||||||
|
|
||||||
// First try to remove existing symlink if it exists
|
// Remove existing file if it exists
|
||||||
if _, err := fs.Stat(currentVaultPath); err == nil {
|
if _, err := fs.Stat(currentVaultPath); err == nil {
|
||||||
secret.Debug("Removing existing current vault symlink", "path", currentVaultPath)
|
secret.Debug("Removing existing currentvault file", "path", currentVaultPath)
|
||||||
// Ignore errors from Remove as we'll try to create/update it anyway.
|
|
||||||
// On some systems, removing a symlink may fail but the subsequent create may still succeed.
|
|
||||||
_ = fs.Remove(currentVaultPath)
|
_ = fs.Remove(currentVaultPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to create a real symlink first (works on Unix systems)
|
// Write the relative path to the file
|
||||||
if _, ok := fs.(*afero.OsFs); ok {
|
secret.Debug("Writing currentvault file", "relative_path", relativePath)
|
||||||
secret.Debug("Creating vault symlink", "target", targetPath, "link", currentVaultPath)
|
if err := afero.WriteFile(fs, currentVaultPath, []byte(relativePath), secret.FilePerms); err != nil {
|
||||||
if err := os.Symlink(targetPath, currentVaultPath); err == nil {
|
|
||||||
secret.Debug("Successfully selected vault", "vault_name", name)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// If symlink creation fails, fall back to regular file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: create a regular file with the target path
|
|
||||||
secret.Debug("Fallback: creating regular file with target path", "target", targetPath)
|
|
||||||
if err := afero.WriteFile(fs, currentVaultPath, []byte(targetPath), secret.FilePerms); err != nil {
|
|
||||||
return fmt.Errorf("failed to select vault: %w", err)
|
return fmt.Errorf("failed to select vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -98,38 +98,28 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
|
|||||||
return unlocker, nil
|
return unlocker, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveUnlockerDirectory resolves the unlocker directory from a symlink or file
|
// resolveUnlockerDirectory reads the current-unlocker file to get the unlocker directory path
|
||||||
|
// The file contains a relative path to the unlocker directory
|
||||||
func (v *Vault) resolveUnlockerDirectory(currentUnlockerPath string) (string, error) {
|
func (v *Vault) resolveUnlockerDirectory(currentUnlockerPath string) (string, error) {
|
||||||
linkReader, ok := v.fs.(afero.LinkReader)
|
secret.Debug("Reading current-unlocker file", "path", currentUnlockerPath)
|
||||||
if !ok {
|
|
||||||
// Fallback for filesystems that don't support symlinks
|
|
||||||
return v.readUnlockerPathFromFile(currentUnlockerPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret.Debug("Resolving unlocker symlink using afero")
|
unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
|
||||||
// Try to read as symlink first
|
|
||||||
unlockerDir, err := linkReader.ReadlinkIfPossible(currentUnlockerPath)
|
|
||||||
if err == nil {
|
|
||||||
return unlockerDir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
secret.Debug("Failed to read symlink, falling back to file contents",
|
|
||||||
"error", err, "symlink_path", currentUnlockerPath)
|
|
||||||
// Fallback: read the path from file contents
|
|
||||||
return v.readUnlockerPathFromFile(currentUnlockerPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readUnlockerPathFromFile reads the unlocker directory path from a file
|
|
||||||
func (v *Vault) readUnlockerPathFromFile(path string) (string, error) {
|
|
||||||
secret.Debug("Reading unlocker path from file", "path", path)
|
|
||||||
unlockerDirBytes, err := afero.ReadFile(v.fs, path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to read unlocker path file", "error", err, "path", path)
|
secret.Debug("Failed to read current-unlocker file", "error", err, "path", currentUnlockerPath)
|
||||||
|
|
||||||
return "", fmt.Errorf("failed to read current unlocker: %w", err)
|
return "", fmt.Errorf("failed to read current unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimSpace(string(unlockerDirBytes)), nil
|
relativePath := strings.TrimSpace(string(unlockerDirBytes))
|
||||||
|
secret.Debug("Read relative path from file", "relative_path", relativePath)
|
||||||
|
|
||||||
|
// Resolve to absolute path relative to the vault directory
|
||||||
|
vaultDir := filepath.Dir(currentUnlockerPath)
|
||||||
|
absolutePath := filepath.Join(vaultDir, relativePath)
|
||||||
|
|
||||||
|
secret.Debug("Resolved to absolute path", "absolute_path", absolutePath)
|
||||||
|
|
||||||
|
return absolutePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findUnlockerByID finds an unlocker by its ID and returns the unlocker instance and its directory path
|
// findUnlockerByID finds an unlocker by its ID and returns the unlocker instance and its directory path
|
||||||
@ -287,30 +277,28 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
|||||||
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create/update current unlocker symlink
|
// Create/update current-unlocker file with relative path
|
||||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||||
|
|
||||||
// Remove existing symlink if it exists
|
// Remove existing file if it exists
|
||||||
if exists, err := afero.Exists(v.fs, currentUnlockerPath); err != nil {
|
if exists, err := afero.Exists(v.fs, currentUnlockerPath); err != nil {
|
||||||
return fmt.Errorf("failed to check if current unlocker symlink exists: %w", err)
|
return fmt.Errorf("failed to check if current-unlocker file exists: %w", err)
|
||||||
} else if exists {
|
} else if exists {
|
||||||
if err := v.fs.Remove(currentUnlockerPath); err != nil {
|
if err := v.fs.Remove(currentUnlockerPath); err != nil {
|
||||||
return fmt.Errorf("failed to remove existing unlocker symlink: %w", err)
|
return fmt.Errorf("failed to remove existing current-unlocker file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new symlink using afero's SymlinkIfPossible
|
// Compute relative path from vault directory to unlocker directory
|
||||||
if linker, ok := v.fs.(afero.Linker); ok {
|
relativePath, err := filepath.Rel(vaultDir, targetUnlockerDir)
|
||||||
secret.Debug("Creating unlocker symlink", "target", targetUnlockerDir, "link", currentUnlockerPath)
|
if err != nil {
|
||||||
if err := linker.SymlinkIfPossible(targetUnlockerDir, currentUnlockerPath); err != nil {
|
return fmt.Errorf("failed to compute relative path: %w", err)
|
||||||
return fmt.Errorf("failed to create unlocker symlink: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: create a regular file with the target path for filesystems that don't support symlinks
|
|
||||||
secret.Debug("Fallback: creating regular file with target path", "target", targetUnlockerDir)
|
|
||||||
if err := afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms); err != nil {
|
|
||||||
return fmt.Errorf("failed to create unlocker symlink file: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write the relative path to the file
|
||||||
|
secret.Debug("Writing current-unlocker file", "relative_path", relativePath)
|
||||||
|
if err := afero.WriteFile(v.fs, currentUnlockerPath, []byte(relativePath), secret.FilePerms); err != nil {
|
||||||
|
return fmt.Errorf("failed to create current-unlocker file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user