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:
+51
-23
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/alias"
|
||||
"github.com/safedep/pmg/internal/doctor"
|
||||
@@ -94,29 +95,7 @@ func runCoreChecks(cfg *config.RuntimeConfig) []doctor.CheckResult {
|
||||
Name: checkEventLogDir,
|
||||
Category: "Configuration",
|
||||
Run: func() doctor.CheckResult {
|
||||
if cfg.Config.SkipEventLogging {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusWarn,
|
||||
Message: "Event logging is disabled",
|
||||
}
|
||||
}
|
||||
info, err := os.Stat(cfg.EventLogDir())
|
||||
if err != nil {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Event log directory not found",
|
||||
}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Event log path is not a directory",
|
||||
}
|
||||
}
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "Event log directory found",
|
||||
}
|
||||
return checkEventLogDirResult(cfg.Config.SkipEventLogging, cfg.EventLogDir(), cfg.ConfigDir())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -278,6 +257,52 @@ func runCoreChecks(cfg *config.RuntimeConfig) []doctor.CheckResult {
|
||||
return doctor.RunChecks(checks)
|
||||
}
|
||||
|
||||
// checkEventLogDirResult is the testable core of the event-log dir check.
|
||||
// Event logging is mandatory (init failure is fatal), so an unwritable dir
|
||||
// fail-closes every pmg command for this user. The common cause is a root or
|
||||
// sudo run having created the per-user directory as root, hence the chown fix.
|
||||
func checkEventLogDirResult(skipEventLogging bool, logDir, configDir string) doctor.CheckResult {
|
||||
if skipEventLogging {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusWarn,
|
||||
Message: "Event logging is disabled",
|
||||
}
|
||||
}
|
||||
info, err := os.Stat(logDir)
|
||||
if err != nil {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Event log directory not found",
|
||||
}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Event log path is not a directory",
|
||||
}
|
||||
}
|
||||
|
||||
probe, err := os.CreateTemp(logDir, ".pmg-doctor-*")
|
||||
if err != nil {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Event log directory not writable",
|
||||
Fix: fmt.Sprintf("sudo chown -R $(id -un) %s", configDir),
|
||||
}
|
||||
}
|
||||
if err := probe.Close(); err != nil {
|
||||
log.Warnf("failed to close doctor probe file: %v", err)
|
||||
}
|
||||
if err := os.Remove(probe.Name()); err != nil {
|
||||
log.Warnf("failed to remove doctor probe file: %v", err)
|
||||
}
|
||||
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "Event log directory found",
|
||||
}
|
||||
}
|
||||
|
||||
func pathContainsDir(pathEntries []string, dir string) bool {
|
||||
if dir == "" {
|
||||
return false
|
||||
@@ -520,6 +545,9 @@ func printResults(results []doctor.CheckResult) {
|
||||
fix := ui.Colors.Dim("—")
|
||||
if r.Status != doctor.StatusPass {
|
||||
fix = fixHint(r.Name)
|
||||
if r.Fix != "" {
|
||||
fix = r.Fix
|
||||
}
|
||||
}
|
||||
rows = append(rows, []string{
|
||||
statusBadge(r.Status),
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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) {
|
||||
@@ -102,3 +105,48 @@ func TestClassifyPackageManagerResolutionsAcceptsEitherShimDir(t *testing.T) {
|
||||
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