mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: actionable remedy for root-created per-user config dir
A pmg run as root with a preserved HOME (GitHub runners, sudo -E, su without -) creates the invoking user's ~/.config/safedep as root-owned, and event-log init then fail-closes every later non-root command. Make that state self-solvable: - event-log init permission errors exit with a usefulerror naming the likely cause and the chown fix instead of a bare fatal - pmg setup doctor probes event-log dir writability and reports the same fix via a new per-result Fix override - document the mechanism and remedy in system-install.md, along with the binary ownership requirements for --system - consolidate this branch's doctor tests into doctor_test.go
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/pmg/internal/doctor"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPathContainsDir(t *testing.T) {
|
||||
assert.True(t, pathContainsDir([]string{"/usr/local/lib/pmg/bin/"}, "/usr/local/lib/pmg/bin"))
|
||||
assert.False(t, pathContainsDir([]string{"/usr/local/bin"}, "/usr/local/lib/pmg/bin"))
|
||||
assert.False(t, pathContainsDir([]string{"/usr/bin"}, ""))
|
||||
}
|
||||
|
||||
func TestPathIsUnderDir(t *testing.T) {
|
||||
assert.True(t, pathIsUnderDir("/usr/local/lib/pmg/bin/npm", "/usr/local/lib/pmg/bin"))
|
||||
assert.False(t, pathIsUnderDir("/usr/local/bin/npm", "/usr/local/lib/pmg/bin"))
|
||||
assert.False(t, pathIsUnderDir("/usr/local/lib/pmg/bin-extra/npm", "/usr/local/lib/pmg/bin"))
|
||||
}
|
||||
|
||||
func TestSystemInstallAliasesPassDoesNotActivateInterception(t *testing.T) {
|
||||
results := []doctor.CheckResult{
|
||||
{Name: checkShellAliases, Status: doctor.StatusPass, Message: "No aliases (system install)"},
|
||||
{Name: checkShimInPath, Status: doctor.StatusFail},
|
||||
}
|
||||
|
||||
assert.False(t, isInterceptionActive(results))
|
||||
}
|
||||
|
||||
func TestAliasesInstalledActivatesInterception(t *testing.T) {
|
||||
results := []doctor.CheckResult{
|
||||
{
|
||||
Name: checkShellAliases,
|
||||
Status: doctor.StatusPass,
|
||||
Message: aliasesInstalledMessage,
|
||||
ImpliesInterception: true,
|
||||
},
|
||||
{Name: checkShimInPath, Status: doctor.StatusFail},
|
||||
}
|
||||
|
||||
assert.True(t, isInterceptionActive(results))
|
||||
}
|
||||
|
||||
func TestShimInPathImpliesInterception(t *testing.T) {
|
||||
results := []doctor.CheckResult{
|
||||
{
|
||||
Name: checkShimInPath,
|
||||
Status: doctor.StatusPass,
|
||||
Message: "Package managers resolve to System shim directory",
|
||||
ImpliesInterception: true,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, isInterceptionActive(results))
|
||||
}
|
||||
|
||||
func TestClassifyPackageManagerResolutions(t *testing.T) {
|
||||
shimDir := "/usr/local/lib/pmg/bin"
|
||||
lookPath := func(name string) (string, error) {
|
||||
switch name {
|
||||
case "npm":
|
||||
return shimDir + "/npm", nil
|
||||
case "pip":
|
||||
return "/usr/bin/pip", nil
|
||||
case "uv":
|
||||
return "", exec.ErrNotFound
|
||||
default:
|
||||
return "", exec.ErrNotFound
|
||||
}
|
||||
}
|
||||
|
||||
under, shadowed := classifyPackageManagerResolutions(
|
||||
[]string{"npm", "pip", "uv"},
|
||||
[]string{shimDir},
|
||||
lookPath,
|
||||
)
|
||||
assert.Equal(t, []string{"npm"}, under)
|
||||
assert.Equal(t, []string{"pip"}, shadowed)
|
||||
}
|
||||
|
||||
func TestClassifyPackageManagerResolutionsAcceptsEitherShimDir(t *testing.T) {
|
||||
systemDir := "/usr/local/lib/pmg/bin"
|
||||
userDir := "/home/dev/.pmg/bin"
|
||||
lookPath := func(name string) (string, error) {
|
||||
switch name {
|
||||
case "npm":
|
||||
return systemDir + "/npm", nil
|
||||
case "pip":
|
||||
return userDir + "/pip", nil
|
||||
default:
|
||||
return "/usr/bin/" + name, nil
|
||||
}
|
||||
}
|
||||
|
||||
under, shadowed := classifyPackageManagerResolutions(
|
||||
[]string{"npm", "pip", "yarn"},
|
||||
[]string{systemDir, userDir},
|
||||
lookPath,
|
||||
)
|
||||
assert.ElementsMatch(t, []string{"npm", "pip"}, under)
|
||||
assert.Equal(t, []string{"yarn"}, shadowed)
|
||||
}
|
||||
|
||||
func TestCheckEventLogDirResult(t *testing.T) {
|
||||
configDir := "/home/dev/.config/safedep/pmg"
|
||||
|
||||
t.Run("skipped when event logging disabled", func(t *testing.T) {
|
||||
result := checkEventLogDirResult(true, t.TempDir(), configDir)
|
||||
assert.Equal(t, doctor.StatusWarn, result.Status)
|
||||
})
|
||||
|
||||
t.Run("missing directory fails", func(t *testing.T) {
|
||||
result := checkEventLogDirResult(false, filepath.Join(t.TempDir(), "absent"), configDir)
|
||||
assert.Equal(t, doctor.StatusFail, result.Status)
|
||||
assert.Equal(t, "Event log directory not found", result.Message)
|
||||
})
|
||||
|
||||
t.Run("file instead of directory fails", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "logs")
|
||||
require.NoError(t, os.WriteFile(path, []byte("x"), 0o644))
|
||||
|
||||
result := checkEventLogDirResult(false, path, configDir)
|
||||
assert.Equal(t, doctor.StatusFail, result.Status)
|
||||
})
|
||||
|
||||
t.Run("writable directory passes", func(t *testing.T) {
|
||||
result := checkEventLogDirResult(false, t.TempDir(), configDir)
|
||||
assert.Equal(t, doctor.StatusPass, result.Status)
|
||||
})
|
||||
|
||||
t.Run("unwritable directory fails with chown fix", func(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("running as root: directory permissions are not enforced")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.Chmod(dir, 0o555))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, os.Chmod(dir, 0o755))
|
||||
})
|
||||
|
||||
result := checkEventLogDirResult(false, dir, configDir)
|
||||
assert.Equal(t, doctor.StatusFail, result.Status)
|
||||
assert.Equal(t, "Event log directory not writable", result.Message)
|
||||
assert.Contains(t, result.Fix, "sudo chown -R")
|
||||
assert.Contains(t, result.Fix, configDir)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user