From b2ee447ac38dfeff0f89746293884508631f6ab4 Mon Sep 17 00:00:00 2001 From: Fabio Forni Date: Thu, 23 Nov 2023 16:43:08 +0100 Subject: [PATCH] stepwriter: Add OS that replicates the current logic --- installer/stepwriter/os.go | 168 +++++++++++++++++++++++++++ installer/stepwriter/os_unix.go | 46 ++++++++ installer/stepwriter/os_unix_test.go | 40 +++++++ installer/stepwriter/os_windows.go | 27 +++++ 4 files changed, 281 insertions(+) create mode 100644 installer/stepwriter/os.go create mode 100644 installer/stepwriter/os_unix.go create mode 100644 installer/stepwriter/os_unix_test.go create mode 100644 installer/stepwriter/os_windows.go diff --git a/installer/stepwriter/os.go b/installer/stepwriter/os.go new file mode 100644 index 0000000..45ac32d --- /dev/null +++ b/installer/stepwriter/os.go @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: Fabio Forni +// SPDX-License-Identifier: MPL-2.0 + +package stepwriter + +import ( + "errors" + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + + "github.com/livingsilver94/backee/installer" + "github.com/livingsilver94/backee/privilege" + "github.com/livingsilver94/backee/service" +) + +func init() { + privilege.RegisterInterfaceImpl(symlinkWriter{}) + privilege.RegisterInterfaceImpl(fileCopyWriter{}) + privilege.RegisterInterfaceImpl(privilegedPathWriter{}) +} + +type OS struct{} + +func (OS) Setup(script string) error { + return runScript(script) +} + +func (OS) InstallPackages(fullCmd []string) error { + return runProcess(fullCmd[0], fullCmd[1:]...) +} + +func (OS) SymlinkFile(dst service.FilePath, src string) error { + return writePossiblyPrivilegedPath(dst, &symlinkWriter{SrcPath: src}) +} + +func (OS) CopyFile(dst service.FilePath, src installer.FileCopy) error { + return writePossiblyPrivilegedPath(dst, &fileCopyWriter{}) +} + +func (OS) Finalize(script string) error { + return runScript(script) +} + +type fileWriter interface { + writeFile(dst string) error +} + +func writePath(dst service.FilePath, wr fileWriter) error { + err := os.MkdirAll(filepath.Dir(dst.Path), 0755) + if err != nil { + return err + } + err = wr.writeFile(dst.Path) + if err != nil { + return err + } + if dst.Mode != 0 { + return os.Chmod(dst.Path, fs.FileMode(dst.Mode)) + } + return nil +} + +func writePathPrivileged(dst service.FilePath, wr fileWriter) error { + var r privilege.Runner = privilegedPathWriter{Dst: dst, Wr: wr} + return privilege.Run(r) +} + +func writePossiblyPrivilegedPath(dst service.FilePath, wr fileWriter) error { + err := writePath(dst, wr) + if err != nil { + if !errors.Is(err, fs.ErrPermission) { + return err + } + err = writePathPrivileged(dst, wr) + if err != nil { + return err + } + } + return nil +} + +type symlinkWriter struct { + SrcPath string +} + +func (w symlinkWriter) writeFile(dst string) error { + err := os.Symlink(w.SrcPath, dst) + if err != nil { + if !errors.Is(err, fs.ErrExist) { + return err + } + eq, errEq := w.isSymlinkEqual(dst) + if errEq != nil { + return errEq + } + if !eq { + return err + } + } + return nil +} + +func (w *symlinkWriter) isSymlinkEqual(dst string) (bool, error) { + eq, err := filepath.EvalSymlinks(dst) + if err != nil { + return false, err + } + return eq == w.SrcPath, nil +} + +type fileCopyWriter struct { + FileCopy installer.FileCopy +} + +func (w fileCopyWriter) writeFile(dst string) error { + file, err := os.Create(dst) + if err != nil { + return err + } + defer file.Close() + _, err = w.FileCopy.WriteTo(file) + return err +} + +type privilegedPathWriter struct { + Dst service.FilePath + Wr fileWriter +} + +func (p privilegedPathWriter) RunPrivileged() error { + return writePath(p.Dst, p.Wr) +} + +func runProcess(name string, arg ...string) error { + cmd := exec.Command(name, arg...) + cmd.Stdout = nil + cmd.Stderr = os.Stderr + return cmd.Run() +} + +type UnixID struct { + UID uint32 + GID uint32 +} + +func PathOwner(path string) (UnixID, error) { + return PathOwnerFS(os.DirFS(path), ".") +} + +func parentPathOwner(path string) (UnixID, error) { + for { + if len(path) == 1 { + return UnixID{}, fmt.Errorf("parent directory of %s: %w", path, fs.ErrNotExist) + } + id, err := PathOwner(path) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return UnixID{}, err + } + path = filepath.Dir(path) + continue + } + return id, nil + } +} diff --git a/installer/stepwriter/os_unix.go b/installer/stepwriter/os_unix.go new file mode 100644 index 0000000..ec3c3aa --- /dev/null +++ b/installer/stepwriter/os_unix.go @@ -0,0 +1,46 @@ +//go:build unix + +// SPDX-FileCopyrightText: Fabio Forni +// SPDX-License-Identifier: MPL-2.0 + +package stepwriter + +import ( + "fmt" + "io/fs" + "syscall" +) + +func runScript(script string) error { + return runProcess( + "sh", + "-e", // Stop script on first error. + "-c", // Run the following script string. + script, + ) +} + +func PathOwnerFS(sys fs.FS, path string) (UnixID, error) { + info, err := fs.Stat(sys, path) + if err != nil { + return UnixID{}, err + } + stat := info.Sys().(*syscall.Stat_t) + return UnixID{UID: stat.Uid, GID: stat.Gid}, nil +} + +func RunAsUnixID(f func() error, id UnixID) error { + oldUID := syscall.Geteuid() + oldGID := syscall.Getegid() + err := syscall.Setegid(int(id.GID)) + if err != nil { + return fmt.Errorf("setting GID %d: %w", id.GID, err) + } + defer syscall.Setegid(oldGID) + err = syscall.Seteuid(int(id.UID)) + if err != nil { + return fmt.Errorf("setting UID %d: %w", id.UID, err) + } + defer syscall.Seteuid(oldUID) + return f() +} diff --git a/installer/stepwriter/os_unix_test.go b/installer/stepwriter/os_unix_test.go new file mode 100644 index 0000000..75ce4f3 --- /dev/null +++ b/installer/stepwriter/os_unix_test.go @@ -0,0 +1,40 @@ +//go:build unix + +// SPDX-FileCopyrightText: Fabio Forni +// SPDX-License-Identifier: MPL-2.0 + +package stepwriter_test + +import ( + "syscall" + "testing" + "testing/fstest" + + "github.com/livingsilver94/backee/installer/stepwriter" +) + +func TestUnixIDsFS(t *testing.T) { + const expUID = 123 + const expGID = 456 + fs := fstest.MapFS{ + "file.txt": &fstest.MapFile{Sys: &syscall.Stat_t{Uid: expUID, Gid: expGID}}, + } + + id, err := stepwriter.PathOwnerFS(fs, "file.txt") + if err != nil { + t.Fatal(err) + } + if id.UID != expUID || id.GID != expGID { + t.Fatalf("expected UID %d and GID %d. Got %d and %d", expUID, expGID, id.UID, id.GID) + } +} + +func TestRunAs(t *testing.T) { + f := func() error { return nil } + uid := syscall.Getuid() + gid := syscall.Getgid() + err := stepwriter.RunAsUnixID(f, stepwriter.UnixID{UID: uint32(uid), GID: uint32(gid)}) + if err != nil { + t.Fatal(err) + } +} diff --git a/installer/stepwriter/os_windows.go b/installer/stepwriter/os_windows.go new file mode 100644 index 0000000..abad238 --- /dev/null +++ b/installer/stepwriter/os_windows.go @@ -0,0 +1,27 @@ +//go:build windows + +// SPDX-FileCopyrightText: Fabio Forni +// SPDX-License-Identifier: MPL-2.0 + +package stepwriter + +import ( + "io/fs" +) + +func runScript(script string) error { + return runProcess( + "powershell", + "-NoLogo", // Hide copyright banner. + "-Command", // Run the following script string. + script, + ) +} + +func PathOwnerFS(sys fs.FS, path string) (UnixID, error) { + return UnixID{}, nil +} + +func RunAsUnixID(f func() error, id UnixID) error { + return f() +}