check / check (push) Successful in 1m58s
`bin/entrypoint.sh` now runs `netwatch-server prepare-data-dir`, which refuses a `DATA_DIR` that is not `/data` or a path below it written in full, then creates `DATA_DIR`, gives `/data` and everything in it to `netwatch`, and sets mode 750 on `/data` and `DATA_DIR`. Every step goes through a Go `os.Root` opened on `/data`, and the modes are set on the opened directories rather than by name, so neither a symbolic link already there nor one a host process swaps in while the container starts can make root create or change anything outside `/data`. The README says which `DATA_DIR` values are accepted. Model: opus-5-5
308 lines
7.1 KiB
Go
308 lines
7.1 KiB
Go
package reportbuf_test
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/netwatch/internal/reportbuf"
|
|
)
|
|
|
|
// reports is the last part of DATA_DIR in these tests, as in the
|
|
// image's /data/reports.
|
|
const reports = "reports"
|
|
|
|
// currentUser is the user the test runs as, the only owner a test not
|
|
// run as root can give files to.
|
|
func currentUser() *user.User {
|
|
return &user.User{
|
|
Uid: strconv.Itoa(os.Getuid()),
|
|
Gid: strconv.Itoa(os.Getgid()),
|
|
}
|
|
}
|
|
|
|
// tempDirMode700 is a new directory in a t.TempDir with mode 0700, so
|
|
// a test can tell that PrepareDataDir left its mode alone.
|
|
func tempDirMode700(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
dir := filepath.Join(t.TempDir(), "d")
|
|
|
|
err := os.Mkdir(dir, 0o700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return dir
|
|
}
|
|
|
|
func requireMode(t *testing.T, path string, want fs.FileMode) {
|
|
t.Helper()
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if info.Mode() != want {
|
|
t.Errorf("%s: mode %v, want %v", path, info.Mode(), want)
|
|
}
|
|
}
|
|
|
|
func requireMissing(t *testing.T, path string) {
|
|
t.Helper()
|
|
|
|
_, err := os.Lstat(path)
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
t.Errorf("%s: created, or Lstat failed: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func requireOwner(t *testing.T, path string, uid, gid uint32) {
|
|
t.Helper()
|
|
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stat, _ := info.Sys().(*syscall.Stat_t)
|
|
if stat.Uid != uid || stat.Gid != gid {
|
|
t.Errorf("%s: owner %d:%d, want %d:%d", path, stat.Uid, stat.Gid,
|
|
uid, gid)
|
|
}
|
|
}
|
|
|
|
func TestPrepareDataDirCreatesDataDir(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
volume := t.TempDir()
|
|
dir := filepath.Join(volume, "a", reports)
|
|
|
|
err := reportbuf.PrepareDataDir(volume, dir, currentUser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
requireMode(t, volume, fs.ModeDir|0o750)
|
|
requireMode(t, dir, fs.ModeDir|0o750)
|
|
}
|
|
|
|
// TestPrepareDataDirSetsModeOfExistingDataDir: a DATA_DIR already on
|
|
// the host with another mode gets the mode too, not only a new one.
|
|
func TestPrepareDataDirSetsModeOfExistingDataDir(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
volume := t.TempDir()
|
|
dir := filepath.Join(volume, reports)
|
|
|
|
err := os.Mkdir(dir, 0o700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = reportbuf.PrepareDataDir(volume, dir, currentUser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
requireMode(t, dir, fs.ModeDir|0o750)
|
|
}
|
|
|
|
func TestPrepareDataDirTakesTheVolumeItself(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
volume := t.TempDir()
|
|
|
|
err := reportbuf.PrepareDataDir(volume, volume, currentUser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
requireMode(t, volume, fs.ModeDir|0o750)
|
|
}
|
|
|
|
// TestPrepareDataDirRefusesDataDirOutsideVolume covers a DATA_DIR that
|
|
// is relative, outside the volume, or not written in full.
|
|
func TestPrepareDataDirRefusesDataDirOutsideVolume(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
volume := tempDirMode700(t)
|
|
for _, dir := range []string{
|
|
reports, volume + "/../new", volume + "/", volume + "//" + reports,
|
|
volume + "/./" + reports, volume + "/" + reports + "/..", volume + "x",
|
|
"/etc",
|
|
} {
|
|
err := reportbuf.PrepareDataDir(volume, dir, currentUser())
|
|
if !errors.Is(err, reportbuf.ErrDataDirOutsideVolume) {
|
|
t.Errorf("%q: error = %v, want ErrDataDirOutsideVolume", dir, err)
|
|
}
|
|
}
|
|
|
|
requireMissing(t, filepath.Join(filepath.Dir(volume), "new"))
|
|
requireMode(t, volume, fs.ModeDir|0o700)
|
|
}
|
|
|
|
// TestPrepareDataDirRefusesLinkOutOfVolume puts a symbolic link to a
|
|
// directory outside the volume on the path to DATA_DIR, written as a
|
|
// full path and as one that climbs out with '..', and as DATA_DIR
|
|
// itself, where the mode would be set through it.
|
|
func TestPrepareDataDirRefusesLinkOutOfVolume(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
climbsOut bool
|
|
link, dir string
|
|
}{
|
|
{"full path", false, "x", "x/reports"},
|
|
{"climbs out", true, "x", "x/reports"},
|
|
{"DATA_DIR itself", false, reports, reports},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
outside := tempDirMode700(t)
|
|
volume := t.TempDir()
|
|
|
|
climbOut, err := filepath.Rel(volume, outside)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
target := outside
|
|
if tc.climbsOut {
|
|
target = climbOut
|
|
}
|
|
|
|
err = os.Symlink(target, filepath.Join(volume, tc.link))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = reportbuf.PrepareDataDir(volume,
|
|
filepath.Join(volume, tc.dir), currentUser())
|
|
if err == nil {
|
|
t.Error("no error")
|
|
}
|
|
|
|
requireMissing(t, filepath.Join(outside, reports))
|
|
requireMode(t, outside, fs.ModeDir|0o700)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPrepareDataDirRefusesDanglingLink: DATA_DIR is a symbolic link
|
|
// to a name in the volume that does not exist, which is not created.
|
|
func TestPrepareDataDirRefusesDanglingLink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
volume := t.TempDir()
|
|
|
|
err := os.Symlink("missing", filepath.Join(volume, reports))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports),
|
|
currentUser())
|
|
if err == nil {
|
|
t.Error("no error")
|
|
}
|
|
|
|
requireMissing(t, filepath.Join(volume, "missing"))
|
|
}
|
|
|
|
// TestPrepareDataDirTakesDirectoryNamedInLatin1: a host directory can
|
|
// hold names that are not valid UTF-8, here "café" written in Latin-1.
|
|
// A test not run as root can only check that PrepareDataDir goes into
|
|
// such a directory and gives it, and what it holds, to the current
|
|
// user.
|
|
func TestPrepareDataDirTakesDirectoryNamedInLatin1(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
volume := t.TempDir()
|
|
latin1 := filepath.Join(volume, "caf\xe9")
|
|
|
|
err := os.Mkdir(latin1, 0o700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = os.WriteFile(filepath.Join(latin1, "f"), nil, 0o600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
owner := currentUser()
|
|
|
|
err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports),
|
|
owner)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
uid, _ := strconv.ParseUint(owner.Uid, 10, 32)
|
|
gid, _ := strconv.ParseUint(owner.Gid, 10, 32)
|
|
|
|
requireOwner(t, latin1, uint32(uid), uint32(gid))
|
|
requireOwner(t, filepath.Join(latin1, "f"), uint32(uid), uint32(gid))
|
|
}
|
|
|
|
// TestPrepareDataDirGivesVolumeToOwner gives everything in the volume
|
|
// to a uid and gid that own nothing, which only root can do. A
|
|
// symbolic link in the volume to a directory outside it is given to
|
|
// them itself; what it points to is left as it was.
|
|
func TestPrepareDataDirGivesVolumeToOwner(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if os.Geteuid() != 0 {
|
|
t.Skip("only root can give files to another uid")
|
|
}
|
|
|
|
outside := t.TempDir()
|
|
volume := t.TempDir()
|
|
old := filepath.Join(volume, "old")
|
|
|
|
err := os.WriteFile(filepath.Join(outside, "f"), nil, 0o600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = os.Mkdir(old, 0o700)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = os.WriteFile(filepath.Join(old, "f"), nil, 0o600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = os.Symlink(outside, filepath.Join(old, "link"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports),
|
|
&user.User{Uid: "4242", Gid: "4343"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, path := range []string{
|
|
volume, filepath.Join(volume, reports), old,
|
|
filepath.Join(old, "f"), filepath.Join(old, "link"),
|
|
} {
|
|
requireOwner(t, path, 4242, 4343)
|
|
}
|
|
|
|
requireOwner(t, outside, 0, 0)
|
|
requireOwner(t, filepath.Join(outside, "f"), 0, 0)
|
|
}
|