Skip to content

Commit

Permalink
stepwriter: Add OS that replicates the current logic
Browse files Browse the repository at this point in the history
  • Loading branch information
livingsilver94 committed Nov 24, 2023
1 parent 0fdbb37 commit b2ee447
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 0 deletions.
168 changes: 168 additions & 0 deletions installer/stepwriter/os.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-FileCopyrightText: Fabio Forni <[email protected]>
// 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
}
}
46 changes: 46 additions & 0 deletions installer/stepwriter/os_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build unix

// SPDX-FileCopyrightText: Fabio Forni <[email protected]>
// 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()
}
40 changes: 40 additions & 0 deletions installer/stepwriter/os_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//go:build unix

// SPDX-FileCopyrightText: Fabio Forni <[email protected]>
// 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)
}
}
27 changes: 27 additions & 0 deletions installer/stepwriter/os_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build windows

// SPDX-FileCopyrightText: Fabio Forni <[email protected]>
// 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()
}

0 comments on commit b2ee447

Please sign in to comment.