165 lines
5.1 KiB
Go
165 lines
5.1 KiB
Go
package profile_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"proxyctl/internal/profile"
|
|
)
|
|
|
|
func TestReadOnlyLoadValidatesWithoutRepairingOrCreatingStorage(t *testing.T) {
|
|
directory := filepath.Join(t.TempDir(), "proxyctl")
|
|
if err := os.MkdirAll(directory, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(directory, "profiles.json")
|
|
if err := os.WriteFile(path, []byte(`{"profiles":[{"name":"office","mode":"single","singleUrl":"http://proxy.test:8080","httpUrl":"http://proxy.test:8080","httpsUrl":"http://proxy.test:8080","allUrl":"http://proxy.test:8080"}],"activeProfile":"office"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := profile.NewFileStore(path)
|
|
state, err := store.ReadOnlyLoad()
|
|
if err != nil || state.ActiveProfile != "office" {
|
|
t.Fatalf("read-only load state=%#v err=%v", state, err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0o644 {
|
|
t.Fatalf("read-only load changed mode to %o", info.Mode().Perm())
|
|
}
|
|
if _, err := os.Stat(path + ".lock"); !os.IsNotExist(err) {
|
|
t.Fatalf("read-only load created lock: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadOnlyLoadPreservesMissingAndMalformedBehavior(t *testing.T) {
|
|
t.Run("missing", func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "missing", "profiles.json")
|
|
state, err := profile.NewFileStore(path).ReadOnlyLoad()
|
|
if err != nil || len(state.Profiles) != 0 || state.ActiveProfile != "" {
|
|
t.Fatalf("missing read-only load state=%#v err=%v", state, err)
|
|
}
|
|
if _, statErr := os.Stat(filepath.Dir(path)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("missing read-only load created storage: %v", statErr)
|
|
}
|
|
})
|
|
|
|
t.Run("malformed", func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "profiles.json")
|
|
if err := os.WriteFile(path, []byte("not JSON"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := profile.NewFileStore(path).ReadOnlyLoad()
|
|
if err == nil || !strings.Contains(err.Error(), "parse Profile configuration") {
|
|
t.Fatalf("malformed read-only load error = %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReadOnlyLoadUsesConfiguredTrustedRoot(t *testing.T) {
|
|
trustedRoot := t.TempDir()
|
|
outside := filepath.Join(t.TempDir(), "profiles.json")
|
|
if err := os.WriteFile(outside, []byte(`{"profiles":[]}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
owner := profile.UserIdentity{HomeDir: trustedRoot, UID: os.Getuid(), GID: os.Getgid()}
|
|
_, err := profile.NewOwnedFileStoreAt(outside, owner, trustedRoot).ReadOnlyLoad()
|
|
if err == nil || !strings.Contains(err.Error(), "escapes its trusted root") {
|
|
t.Fatalf("configured trusted root was not enforced: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadOnlyLoadRejectsSymlinksWithoutReadingOrChangingVictim(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
path func(t *testing.T, root, victim string) string
|
|
}{
|
|
{
|
|
name: "final symlink",
|
|
path: func(t *testing.T, root, victim string) string {
|
|
t.Helper()
|
|
directory := filepath.Join(root, "proxyctl")
|
|
if err := os.MkdirAll(directory, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(directory, "profiles.json")
|
|
if err := os.Symlink(victim, path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
},
|
|
},
|
|
{
|
|
name: "ancestor symlink",
|
|
path: func(t *testing.T, root, victim string) string {
|
|
t.Helper()
|
|
link := filepath.Join(root, "proxyctl")
|
|
if err := os.Symlink(filepath.Dir(victim), link); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return filepath.Join(link, filepath.Base(victim))
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
victim := filepath.Join(t.TempDir(), "profiles.json")
|
|
const content = "victim content that is deliberately not JSON"
|
|
if err := os.WriteFile(victim, []byte(content), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(victim, time.Unix(1, 0), time.Now()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := fileIdentity(t, victim)
|
|
path := test.path(t, t.TempDir(), victim)
|
|
|
|
_, err := profile.NewFileStore(path).ReadOnlyLoad()
|
|
if err == nil {
|
|
t.Fatal("ReadOnlyLoad accepted a symlinked Profile configuration")
|
|
}
|
|
if strings.Contains(err.Error(), "parse Profile configuration") {
|
|
t.Fatalf("ReadOnlyLoad followed and parsed the victim: %v", err)
|
|
}
|
|
after := fileIdentity(t, victim)
|
|
if before != after {
|
|
t.Fatalf("victim was read or its metadata changed: before=%+v after=%+v", before, after)
|
|
}
|
|
got, readErr := os.ReadFile(victim)
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if string(got) != content {
|
|
t.Fatalf("victim content changed to %q", got)
|
|
}
|
|
if _, statErr := os.Lstat(path + ".lock"); !os.IsNotExist(statErr) {
|
|
t.Fatalf("ReadOnlyLoad created a lock: %v", statErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type diagnosticFileIdentity struct {
|
|
mode os.FileMode
|
|
uid uint32
|
|
gid uint32
|
|
atime syscall.Timespec
|
|
}
|
|
|
|
func fileIdentity(t *testing.T, path string) diagnosticFileIdentity {
|
|
t.Helper()
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
if !ok {
|
|
t.Fatalf("stat data has type %T", info.Sys())
|
|
}
|
|
return diagnosticFileIdentity{mode: info.Mode(), uid: stat.Uid, gid: stat.Gid, atime: stat.Atim}
|
|
}
|