Parents the host makes inside an owner's home are the owner's (hq ADR 0182, to-be 41)
A file or archive placed under a fresh account's home with an owner left the parents it created, such as ~/.config or ~/.local/share, owned by root, so the person's own programs could not write there. Parents that already existed, and any outside the owner's home, are left as before.
This commit is contained in:
@@ -772,7 +772,7 @@ func applyDirectory(r *declaration.Directory) (Outcome, error) {
|
||||
}
|
||||
|
||||
if !existed {
|
||||
if err := os.MkdirAll(r.Path, mode); err != nil {
|
||||
if err := makeDirs(r.Path, mode, r.Owner); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
@@ -968,7 +968,7 @@ func applyFile(r *declaration.File, previous store.Applied, unseal Unseal, keepF
|
||||
return out, fmt.Errorf("keeping the original of %s before writing over it: %w", r.Path, err)
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(r.Path), 0o755); err != nil {
|
||||
if err := makeDirs(filepath.Dir(r.Path), 0o755, r.Owner); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := writeAtomically(r.Path, []byte(content), mode); err != nil {
|
||||
|
||||
@@ -89,7 +89,7 @@ func applyArchive(ctx context.Context, r *declaration.Archive, previous store.Ap
|
||||
// removed only once the new one is in place. A failed unpack leaves the old tree untouched.
|
||||
func replaceWith(body []byte, path, owner string) (int, error) {
|
||||
parent := filepath.Dir(path)
|
||||
if err := os.MkdirAll(parent, 0o755); err != nil {
|
||||
if err := makeDirs(parent, 0o755, owner); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fresh := path + ".unpacking"
|
||||
|
||||
@@ -183,7 +183,7 @@ func applyBlock(r *declaration.File, previous store.Applied) (Outcome, error) {
|
||||
} else if mode, err = modeOf(r.Mode, mode); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(real), 0o755); err != nil {
|
||||
if err := makeDirs(filepath.Dir(real), 0o755, r.Owner); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := writeAtomically(real, []byte(next), mode); err != nil {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
osuser "os/user"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0182 and to-be 41: a parent the host makes inside an owner's home is the
|
||||
// owner's, one that was there is held as found, and one outside the home is made as before.
|
||||
|
||||
// aHome gives the account running the test a home in a directory the test owns, and writes down
|
||||
// every directory the host gives to whom. The account's own name, so what the host chowns resolves
|
||||
// without being root; the record, so what was given is told apart from what was merely made.
|
||||
func aHome(t *testing.T) (home, owner string, given map[string]string) {
|
||||
t.Helper()
|
||||
me, err := osuser.Current()
|
||||
if err != nil {
|
||||
t.Skip("no current user to own anything")
|
||||
}
|
||||
home = t.TempDir()
|
||||
given = map[string]string{}
|
||||
wasHome, wasOwn := homeOf, ownMade
|
||||
homeOf = func(name string) (string, error) {
|
||||
if name == me.Username {
|
||||
return home, nil
|
||||
}
|
||||
return wasHome(name)
|
||||
}
|
||||
ownMade = func(path, owner string) error {
|
||||
given[path] = owner
|
||||
return wasOwn(path, owner)
|
||||
}
|
||||
t.Cleanup(func() { homeOf, ownMade = wasHome, wasOwn })
|
||||
return home, me.Username, given
|
||||
}
|
||||
|
||||
func givenPaths(given map[string]string) []string {
|
||||
var paths []string
|
||||
for p := range given {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
func TestAFileUnderAHomeGivesTheParentsItMadeToItsOwner(t *testing.T) {
|
||||
home, owner, given := aHome(t)
|
||||
target := filepath.Join(home, ".config", "mesh", "environment.sh")
|
||||
d := parse(t, `{"declaration":1,"resources":[{"id":"shell.env","type":"file","path":"`+target+
|
||||
`","content":"export A=1\n","owner":"`+owner+`"}]}`)
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared,
|
||||
noServices, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{filepath.Join(home, ".config"), filepath.Join(home, ".config", "mesh")}
|
||||
if got := givenPaths(given); strings.Join(got, ",") != strings.Join(want, ",") {
|
||||
t.Errorf("given to the owner: %v, want %v", got, want)
|
||||
}
|
||||
for _, p := range want {
|
||||
if given[p] != owner {
|
||||
t.Errorf("%s given to %q", p, given[p])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnArchiveUnderAHomeGivesTheParentsItMadeToItsOwner(t *testing.T) {
|
||||
home, owner, given := aHome(t)
|
||||
body, digest := anArchive(t, map[string]string{"p10k.zsh": "theme"})
|
||||
target := filepath.Join(home, ".local", "share", "powerlevel10k")
|
||||
d := declare(t, `{"id":"shell.theme","type":"archive","source":"`+serving(t, body)+
|
||||
`","digest":"`+digest+`","path":"`+target+`","owner":"`+owner+`"}`)
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared,
|
||||
noServices, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range []string{filepath.Join(home, ".local"), filepath.Join(home, ".local", "share")} {
|
||||
if given[p] != owner {
|
||||
t.Errorf("%s, made by the host, was not given to the owner: %v", p, givenPaths(given))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAParentThatWasThereIsHeldAsFound(t *testing.T) {
|
||||
home, owner, given := aHome(t)
|
||||
config := filepath.Join(home, ".config")
|
||||
if err := os.Mkdir(config, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := filepath.Join(config, "mesh", "environment.sh")
|
||||
d := parse(t, `{"declaration":1,"resources":[{"id":"shell.env","type":"file","path":"`+target+
|
||||
`","content":"export A=1\n","owner":"`+owner+`"}]}`)
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared,
|
||||
noServices, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, touched := given[config]; touched {
|
||||
t.Error("a parent that was already there was given to the owner")
|
||||
}
|
||||
if info, _ := os.Stat(config); info.Mode().Perm() != 0o700 {
|
||||
t.Errorf("a parent that was already there changed mode: %o", info.Mode().Perm())
|
||||
}
|
||||
if given[filepath.Join(config, "mesh")] != owner {
|
||||
t.Errorf("the parent the host made was not given to the owner: %v", givenPaths(given))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAParentOutsideTheHomeIsMadeAsBefore(t *testing.T) {
|
||||
_, owner, given := aHome(t)
|
||||
target := filepath.Join(t.TempDir(), "var", "lib", "module", "settings.conf")
|
||||
d := parse(t, `{"declaration":1,"resources":[{"id":"module.conf","type":"file","path":"`+target+
|
||||
`","content":"a=1\n","owner":"`+owner+`"}]}`)
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared,
|
||||
noServices, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(given) != 0 {
|
||||
t.Errorf("parents outside the owner's home were given to it: %v", givenPaths(given))
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func applyInto(r *declaration.File, previous store.Applied) (Outcome, error) {
|
||||
mode = m
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(r.Path), 0o755); err != nil {
|
||||
if err := makeDirs(filepath.Dir(r.Path), 0o755, r.Owner); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := writeAtomically(r.Path, want, mode); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
osuser "os/user"
|
||||
@@ -258,6 +259,68 @@ func ownedBy(path, owner string) (bool, error) {
|
||||
return uid == wantUID && gid == wantGID, nil
|
||||
}
|
||||
|
||||
// makeDirs makes a directory and any parent of it that is missing, as MkdirAll does — and gives
|
||||
// each one it made inside the owner's home to the owner (novox/hq ADR 0182, to-be 41).
|
||||
//
|
||||
// **A parent made as root inside a home is a home the person cannot use.** A module writing
|
||||
// ~/.config/mesh/environment.sh, or unpacking into ~/.local/share/powerlevel10k, on a fresh account
|
||||
// made ~/.config and ~/.local/share owned by root: the file was the person's, the directory every
|
||||
// program of theirs writes into was not. So what the host creates between the home and the target
|
||||
// is the owner's, as the target is.
|
||||
//
|
||||
// **Only what the host created.** A parent that was already there is never chowned or chmodded:
|
||||
// what a person or another program made is held as found (ADR 0182). And only inside the owner's
|
||||
// home, read from the user database, not guessed from a prefix on /home: a module's directory under
|
||||
// /var/lib is made exactly as before, whoever its files belong to.
|
||||
func makeDirs(dir string, mode os.FileMode, owner string) error {
|
||||
var made []string
|
||||
for d := filepath.Clean(dir); ; d = filepath.Dir(d) {
|
||||
if _, err := os.Lstat(d); !errors.Is(err, os.ErrNotExist) {
|
||||
break
|
||||
}
|
||||
made = append(made, d)
|
||||
if filepath.Dir(d) == d {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(dir, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
if owner == "" || len(made) == 0 {
|
||||
return nil
|
||||
}
|
||||
home, err := homeOf(owner)
|
||||
if err != nil || home == "" {
|
||||
// A numeric owner — a container's user — has no home, and a name the machine does not
|
||||
// know fails where the target is given to it. Either way nothing here is a home's.
|
||||
return nil
|
||||
}
|
||||
home = filepath.Clean(home)
|
||||
for _, d := range made {
|
||||
if d != home && !strings.HasPrefix(d, home+string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
if err := ownMade(d, owner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// homeOf is an owner's home from the user database, and ownMade gives a directory the host made to
|
||||
// its owner. Variables so a test can give an owner a home it owns, and see what was given to whom
|
||||
// without being root.
|
||||
var (
|
||||
homeOf = func(owner string) (string, error) {
|
||||
found, err := osuser.Lookup(owner)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return found.HomeDir, nil
|
||||
}
|
||||
ownMade = own
|
||||
)
|
||||
|
||||
// ownAll gives a whole tree to a user, for an archive that was unpacked into it.
|
||||
func ownAll(root, owner string) error {
|
||||
if owner == "" {
|
||||
|
||||
Reference in New Issue
Block a user