From 949a5aee61a03d863ce5c512a781efc5a9bb5d4b Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 23 Dec 2025 11:53:28 +0700 Subject: [PATCH] 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 --- internal/cli/integration_test.go | 175 +++++++++++------------------ internal/secret/version.go | 39 ++----- internal/vault/integration_test.go | 37 +++--- internal/vault/management.go | 131 ++++----------------- internal/vault/unlockers.go | 70 +++++------- internal/vault/vault_error_test.go | 2 +- 6 files changed, 144 insertions(+), 310 deletions(-) diff --git a/internal/cli/integration_test.go b/internal/cli/integration_test.go index e8856b4..47881c2 100644 --- a/internal/cli/integration_test.go +++ b/internal/cli/integration_test.go @@ -332,16 +332,12 @@ func test01Initialize(t *testing.T, tempDir, testMnemonic, testPassphrase string defaultVaultDir := filepath.Join(vaultsDir, "default") verifyFileExists(t, defaultVaultDir) - // Check currentvault symlink - it may be absolute or relative - currentVaultLink := filepath.Join(tempDir, "currentvault") - target, err := os.Readlink(currentVaultLink) - require.NoError(t, err, "should be able to read currentvault symlink") - // Check if it points to the right place (handle both absolute and relative) - if filepath.IsAbs(target) { - assert.Equal(t, filepath.Join(tempDir, "vaults.d/default"), target) - } else { - assert.Equal(t, "vaults.d/default", target) - } + // Check currentvault file contains the relative path + currentVaultFile := filepath.Join(tempDir, "currentvault") + targetBytes, err := os.ReadFile(currentVaultFile) + require.NoError(t, err, "should be able to read currentvault file") + target := string(targetBytes) + assert.Equal(t, "vaults.d/default", target, "currentvault should contain relative path") // Verify vault structure 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") verifyFileExists(t, encryptedLTPubKey) - // Check current-unlocker file + // Check current-unlocker file contains the relative path currentUnlockerFile := filepath.Join(defaultVaultDir, "current-unlocker") 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) - t.Logf("DEBUG: current-unlocker file content: %q", string(currentUnlockerContent)) - 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") - } + currentUnlockerContent := readFile(t, currentUnlockerFile) + assert.Contains(t, string(currentUnlockerContent), "passphrase", "current unlocker should point to passphrase type") // Verify vault-metadata.json in vault 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") verifyFileExists(t, workVaultDir) - // Check currentvault symlink was updated - currentVaultLink := filepath.Join(tempDir, "currentvault") - target, err := os.Readlink(currentVaultLink) - require.NoError(t, err, "should be able to read currentvault symlink") - - // The symlink should now point to work vault - if filepath.IsAbs(target) { - assert.Equal(t, filepath.Join(tempDir, "vaults.d/work"), target) - } else { - assert.Equal(t, "vaults.d/work", target) - } + // Check currentvault file was updated + currentVaultFile := filepath.Join(tempDir, "currentvault") + targetBytes, err := os.ReadFile(currentVaultFile) + require.NoError(t, err, "should be able to read currentvault file") + target := string(targetBytes) + assert.Equal(t, "vaults.d/work", target, "currentvault should contain relative path to work") // Verify work vault has basic structure 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") verifyFileExists(t, metadataFile) - // Check current symlink + // Check current file currentLink := filepath.Join(secretDir, "current") verifyFileExists(t, currentLink) - // Verify symlink points to the version directory - target, err := os.Readlink(currentLink) - require.NoError(t, err, "should read current symlink") + // Verify current file contains the version path + targetBytes, err := os.ReadFile(currentLink) + require.NoError(t, err, "should read current file") + target := string(targetBytes) 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 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")) } - // Check current symlink points to new version + // Check current file points to new version currentLink := filepath.Join(secretDir, "current") - target, err := os.Readlink(currentLink) - require.NoError(t, err, "should read current symlink") + targetBytes, err := os.ReadFile(currentLink) + require.NoError(t, err, "should read current file") + target := string(targetBytes) 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 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) currentLink := filepath.Join(defaultVaultDir, "secrets.d", "database%password", "current") - target, err := os.Readlink(currentLink) - require.NoError(t, err, "should read current symlink") + targetBytes, err := os.ReadFile(currentLink) + 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") // 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, version001, "should mention the promoted version") - // Verify symlink was updated - newTarget, err := os.Readlink(currentLink) - require.NoError(t, err, "should read current symlink after promotion") + // Verify current file was updated + newTargetBytes, err := os.ReadFile(currentLink) + require.NoError(t, err, "should read current file after promotion") + newTarget := string(newTargetBytes) 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 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") // Verify current vault is default - currentVaultLink := filepath.Join(tempDir, "currentvault") - target, err := os.Readlink(currentVaultLink) - require.NoError(t, err, "should read currentvault symlink") - if filepath.IsAbs(target) { - assert.Contains(t, target, "vaults.d/default") - } else { - assert.Contains(t, target, "default") - } + currentVaultFile := filepath.Join(tempDir, "currentvault") + targetBytes, err := os.ReadFile(currentVaultFile) + require.NoError(t, err, "should read currentvault file") + target := string(targetBytes) + assert.Equal(t, "vaults.d/default", target, "currentvault should point to default") // Switch to work vault _, err = runSecret("vault", "select", "work") require.NoError(t, err, "vault select work should succeed") // Verify current vault is now work - target, err = os.Readlink(currentVaultLink) - require.NoError(t, err, "should read currentvault symlink") - if filepath.IsAbs(target) { - assert.Contains(t, target, "vaults.d/work") - } else { - assert.Contains(t, target, "work") - } + targetBytes, err = os.ReadFile(currentVaultFile) + require.NoError(t, err, "should read currentvault file") + target = string(targetBytes) + assert.Equal(t, "vaults.d/work", target, "currentvault should point to work") // Switch back to 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) { - // Test currentvault symlink - currentVaultLink := filepath.Join(tempDir, "currentvault") - verifyFileExists(t, currentVaultLink) + // Test currentvault file + currentVaultFile := filepath.Join(tempDir, "currentvault") + verifyFileExists(t, currentVaultFile) - // Read the symlink - target, err := os.Readlink(currentVaultLink) - require.NoError(t, err, "should read currentvault symlink") + // Read the file + targetBytes, err := os.ReadFile(currentVaultFile) + require.NoError(t, err, "should read currentvault file") + target := string(targetBytes) 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") secretDir := filepath.Join(defaultVaultDir, "secrets.d", "database%password") currentLink := filepath.Join(secretDir, "current") verifyFileExists(t, currentLink) - target, err = os.Readlink(currentLink) - require.NoError(t, err, "should read current version symlink") + targetBytes, err = os.ReadFile(currentLink) + require.NoError(t, err, "should read current version file") + target = string(targetBytes) assert.Contains(t, target, "versions", "should point to versions directory") - // Test that symlinks update properly + // Test that current file updates properly // Add new version cmd := exec.Command(secretPath, "add", "database/password", "--force") cmd.Env = []string{ @@ -2038,11 +2019,12 @@ func test29SymlinkHandling(t *testing.T, tempDir, secretPath, testMnemonic strin _, err = cmd.CombinedOutput() require.NoError(t, err, "add new version should succeed") - // Check that symlink was updated - newTarget, err := os.Readlink(currentLink) - require.NoError(t, err, "should read updated symlink") - assert.NotEqual(t, target, newTarget, "symlink should point to new version") - assert.Contains(t, newTarget, "versions", "new symlink should still point to versions directory") + // Check that current file was updated + newTargetBytes, err := os.ReadFile(currentLink) + require.NoError(t, err, "should read updated current file") + newTarget := string(newTargetBytes) + 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)) { @@ -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")) require.NoError(t, err, "backup vaults should succeed") - // Also backup the currentvault symlink/file + // Also backup the currentvault file currentVaultSrc := filepath.Join(tempDir, "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) - writeFile(t, currentVaultDst, data) - } + data := readFile(t, currentVaultSrc) + writeFile(t, currentVaultDst, data) // Add more secrets after backup cmd := exec.Command(secretPath, "add", "post-backup/secret", "--force") @@ -2119,13 +2094,8 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string, // Restore currentvault os.Remove(currentVaultSrc) - if target, err := os.Readlink(currentVaultDst); err == nil { - err = os.Symlink(target, currentVaultSrc) - require.NoError(t, err, "restore currentvault symlink should succeed") - } else { - data := readFile(t, currentVaultDst) - writeFile(t, currentVaultSrc, data) - } + restoredData := readFile(t, currentVaultDst) + writeFile(t, currentVaultSrc, restoredData) // Verify original secrets are restored output, err = runSecretWithEnv(map[string]string{ @@ -2267,18 +2237,7 @@ func copyDir(src, dst string) error { srcPath := filepath.Join(src, entry.Name()) dstPath := filepath.Join(dst, entry.Name()) - // Check if it's a symlink - 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() { + if entry.IsDir() { err = copyDir(srcPath, dstPath) if err != nil { return err diff --git a/internal/secret/version.go b/internal/secret/version.go index 8ab48f8..8ad51a1 100644 --- a/internal/secret/version.go +++ b/internal/secret/version.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "log/slog" - "os" "path/filepath" "sort" "strings" @@ -431,59 +430,37 @@ func ListVersions(fs afero.Fs, secretDir string) ([]string, error) { 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) { currentPath := filepath.Join(secretDir, "current") - // Try to read as a real symlink first - if _, ok := fs.(*afero.OsFs); ok { - target, err := os.Readlink(currentPath) - if err == nil { - // Extract version from path (e.g., "versions/20231215.001" -> "20231215.001") - 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) - } - } - - // 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) + return "", fmt.Errorf("failed to read current version file: %w", err) } target := strings.TrimSpace(string(fileData)) - // Extract version from path + // Extract version from path (e.g., "versions/20231215.001" -> "20231215.001") 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) + return "", fmt.Errorf("invalid current version file 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 { currentPath := filepath.Join(secretDir, "current") targetPath := filepath.Join("versions", version) - // Remove existing symlink if it exists + // Remove existing file if it exists _ = fs.Remove(currentPath) - // Try to create a real symlink first (works on Unix systems) - 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) + // Write the relative path to the file 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 diff --git a/internal/vault/integration_test.go b/internal/vault/integration_test.go index 1ea8122..732323f 100644 --- a/internal/vault/integration_test.go +++ b/internal/vault/integration_test.go @@ -26,9 +26,9 @@ func TestVaultWithRealFilesystem(t *testing.T) { t.Setenv(secret.EnvMnemonic, testMnemonic) t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") - // Test symlink handling - t.Run("SymlinkHandling", func(t *testing.T) { - stateDir := filepath.Join(tempDir, "symlink-test") + // Test currentvault file handling (plain file with relative path) + t.Run("CurrentVaultFileHandling", func(t *testing.T) { + stateDir := filepath.Join(tempDir, "currentvault-test") if err := os.MkdirAll(stateDir, 0o700); err != nil { 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) } - // Create a symlink to the vault directory in a different location - symlinkPath := filepath.Join(tempDir, "test-symlink") - if err := os.Symlink(vaultDir, symlinkPath); err != nil { - t.Fatalf("Failed to create symlink: %v", err) + // Verify the currentvault file exists and contains the right relative path + currentVaultPath := filepath.Join(stateDir, "currentvault") + currentVaultContents, err := os.ReadFile(currentVaultPath) + if err != nil { + t.Fatalf("Failed to read currentvault file: %v", err) } - // Test that we can resolve the symlink correctly - resolvedPath, err := vault.ResolveVaultSymlink(fs, symlinkPath) - if err != nil { - t.Fatalf("Failed to resolve symlink: %v", err) + expectedRelativePath := "vaults.d/test-vault" + if string(currentVaultContents) != expectedRelativePath { + t.Errorf("Expected currentvault to contain %q, got %q", expectedRelativePath, string(currentVaultContents)) } - // On some platforms, the resolved path might have different case or format - // We'll use filepath.EvalSymlinks to get the canonical path for comparison - expectedPath, err := filepath.EvalSymlinks(vaultDir) + // Test that ResolveVaultSymlink correctly resolves the path + resolvedPath, err := vault.ResolveVaultSymlink(fs, currentVaultPath) if err != nil { - t.Fatalf("Failed to evaluate symlink: %v", err) - } - actualPath, err := filepath.EvalSymlinks(resolvedPath) - if err != nil { - t.Fatalf("Failed to evaluate resolved path: %v", err) + t.Fatalf("Failed to resolve currentvault path: %v", err) } - if actualPath != expectedPath { - t.Errorf("Expected symlink to resolve to %s, got %s", expectedPath, actualPath) + if resolvedPath != vaultDir { + t.Errorf("Expected resolved path to be %s, got %s", vaultDir, resolvedPath) } }) diff --git a/internal/vault/management.go b/internal/vault/management.go index cb82bbd..ce22315 100644 --- a/internal/vault/management.go +++ b/internal/vault/management.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "time" "git.eeqj.de/sneak/secret/internal/secret" @@ -31,104 +32,31 @@ func isValidVaultName(name string) bool { return matched } -// resolveRelativeSymlink resolves a relative symlink target to an absolute path -func resolveRelativeSymlink(symlinkPath, _ string) (string, error) { - // Get the current directory before changing - originalDir, err := os.Getwd() +// ResolveVaultSymlink reads the currentvault file to get the path to the current vault +// The file contains a relative path to the vault directory +func ResolveVaultSymlink(fs afero.Fs, currentVaultPath string) (string, error) { + secret.Debug("resolveVaultSymlink starting", "path", currentVaultPath) + + fileData, err := afero.ReadFile(fs, currentVaultPath) if err != nil { - return "", fmt.Errorf("failed to get current directory: %w", err) - } - secret.Debug("Got current directory", "original_dir", originalDir) + secret.Debug("Failed to read currentvault file", "error", err) - // Change to the symlink's directory - 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) + return "", fmt.Errorf("failed to read currentvault file: %w", err) } - secret.Debug("Changed to symlink directory successfully - os.Chdir completed") - // Get the absolute path of the target - secret.Debug("Getting absolute path of current directory") - absolutePath, err := os.Getwd() - if err != nil { - // Try to restore original directory before returning error - _ = os.Chdir(originalDir) + // The file contains a relative path like "vaults.d/default" + relativePath := strings.TrimSpace(string(fileData)) + secret.Debug("Read relative path from file", "relative_path", relativePath) - return "", fmt.Errorf("failed to get absolute path: %w", err) - } - secret.Debug("Got absolute path", "absolute_path", absolutePath) + // Resolve to absolute path relative to the state directory + stateDir := filepath.Dir(currentVaultPath) + absolutePath := filepath.Join(stateDir, relativePath) - // Restore the original directory - 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") + secret.Debug("Resolved to absolute path", "absolute_path", absolutePath) 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 func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) { 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) } - // Create or update the current vault symlink/file + // Create or update the currentvault file with the relative path 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 { - secret.Debug("Removing existing current vault symlink", "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. + secret.Debug("Removing existing currentvault file", "path", currentVaultPath) _ = fs.Remove(currentVaultPath) } - // Try to create a real symlink first (works on Unix systems) - if _, ok := fs.(*afero.OsFs); ok { - secret.Debug("Creating vault symlink", "target", targetPath, "link", currentVaultPath) - 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 { + // Write the relative path to the file + secret.Debug("Writing currentvault file", "relative_path", relativePath) + if err := afero.WriteFile(fs, currentVaultPath, []byte(relativePath), secret.FilePerms); err != nil { return fmt.Errorf("failed to select vault: %w", err) } diff --git a/internal/vault/unlockers.go b/internal/vault/unlockers.go index f4e008a..04e13cb 100644 --- a/internal/vault/unlockers.go +++ b/internal/vault/unlockers.go @@ -98,38 +98,28 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) { 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) { - linkReader, ok := v.fs.(afero.LinkReader) - if !ok { - // Fallback for filesystems that don't support symlinks - return v.readUnlockerPathFromFile(currentUnlockerPath) - } + secret.Debug("Reading current-unlocker file", "path", currentUnlockerPath) - secret.Debug("Resolving unlocker symlink using afero") - // 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) + unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath) 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 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 @@ -287,30 +277,28 @@ func (v *Vault) SelectUnlocker(unlockerID string) error { 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") - // Remove existing symlink if it exists + // Remove existing file if it exists 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 { 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 - if linker, ok := v.fs.(afero.Linker); ok { - secret.Debug("Creating unlocker symlink", "target", targetUnlockerDir, "link", currentUnlockerPath) - if err := linker.SymlinkIfPossible(targetUnlockerDir, currentUnlockerPath); err != nil { - 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) - } + // Compute relative path from vault directory to unlocker directory + relativePath, err := filepath.Rel(vaultDir, targetUnlockerDir) + if err != nil { + return fmt.Errorf("failed to compute relative path: %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 diff --git a/internal/vault/vault_error_test.go b/internal/vault/vault_error_test.go index 72be622..67123b3 100644 --- a/internal/vault/vault_error_test.go +++ b/internal/vault/vault_error_test.go @@ -84,4 +84,4 @@ func TestAddSecretCleansUpOnFailure(t *testing.T) { secretDir := filepath.Join(vaultDir, "secrets.d", secretName) exists, _ := afero.DirExists(fs, secretDir) assert.False(t, exists, "Secret directory should not exist after failed AddSecret") -} \ No newline at end of file +}