Unify trunk on main: initialization → main #3

Merged
jschoubben merged 58 commits from initialization into main 2026-09-05 01:13:33 +00:00
2 changed files with 81 additions and 13 deletions
Showing only changes of commit aca9eb37ff - Show all commits
+47 -13
View File
@@ -7,6 +7,7 @@ import (
osuser "os/user"
"path/filepath"
"strconv"
"strings"
"github.com/novox/mesh-host/internal/declaration"
"github.com/novox/mesh-host/internal/system"
@@ -97,18 +98,9 @@ func own(path, owner string) error {
if owner == "" {
return nil
}
found, err := osuser.Lookup(owner)
uid, gid, err := idsOf(owner)
if err != nil {
return fmt.Errorf("%s should belong to %q and this machine has no such user: %w",
path, owner, err)
}
uid, err := strconv.Atoi(found.Uid)
if err != nil {
return err
}
gid, err := strconv.Atoi(found.Gid)
if err != nil {
return err
return fmt.Errorf("%s should belong to %q: %w", path, owner, err)
}
if err := os.Chown(path, uid, gid); err != nil {
return fmt.Errorf("cannot give %s to %q: %w", path, owner, err)
@@ -116,12 +108,54 @@ func own(path, owner string) error {
return nil
}
// idsOf resolves an owner to a uid and gid: a name this machine knows, or numbers it does not.
//
// **Numbers, because a container's user is a number the machine has never heard of.** A directory
// a module mounts into its container belongs to whoever runs inside — grafana's 472, redis's 999,
// www-data's 33 — and none of those has a row in this machine's passwd, so there is no name to
// look up and none to create. Refusing them looked principled and meant every module whose
// container drops privileges could not own its own data: the store's config was unreadable to
// the store, and the forge could not traverse into the directory that held its files.
//
// "uid:gid" and bare "uid" are numeric; anything else is a name, resolved as before.
func idsOf(owner string) (int, int, error) {
user, group, both := strings.Cut(owner, ":")
if uid, err := strconv.Atoi(user); err == nil {
gid := uid
if both {
g, err := strconv.Atoi(group)
if err != nil {
return 0, 0, fmt.Errorf(
"%q reads as a uid with a group that is not a gid", owner)
}
gid = g
}
return uid, gid, nil
}
if both {
return 0, 0, fmt.Errorf("%q mixes a name with a colon; a name stands alone", owner)
}
found, err := osuser.Lookup(owner)
if err != nil {
return 0, 0, fmt.Errorf("this machine has no such user: %w", err)
}
uid, err := strconv.Atoi(found.Uid)
if err != nil {
return 0, 0, err
}
gid, err := strconv.Atoi(found.Gid)
if err != nil {
return 0, 0, err
}
return uid, gid, nil
}
// ownedBy reports whether a path already belongs to a user, so applying twice changes nothing.
func ownedBy(path, owner string) (bool, error) {
if owner == "" {
return true, nil
}
found, err := osuser.Lookup(owner)
wantUID, wantGID, err := idsOf(owner)
if err != nil {
return false, nil
}
@@ -133,7 +167,7 @@ func ownedBy(path, owner string) (bool, error) {
if !ok {
return false, nil
}
return strconv.Itoa(uid) == found.Uid && strconv.Itoa(gid) == found.Gid, nil
return uid == wantUID && gid == wantGID, nil
}
// ownAll gives a whole tree to a user, for an archive that was unpacked into it.
+34
View File
@@ -0,0 +1,34 @@
package apply
import "testing"
// A container's user is a number the machine has never heard of — grafana's 472, redis's 999 —
// so an owner must be expressible without a passwd row. Refusing numerics looked principled and
// meant every module whose container drops privileges could not own its own data.
func TestAnOwnerMayBeANumberTheMachineDoesNotKnow(t *testing.T) {
for owner, want := range map[string][2]int{
"472:472": {472, 472},
"1000:1000": {1000, 1000},
"999": {999, 999},
"10001:10001": {10001, 10001},
"33:0": {33, 0},
} {
uid, gid, err := idsOf(owner)
if err != nil {
t.Errorf("%q refused: %v", owner, err)
continue
}
if uid != want[0] || gid != want[1] {
t.Errorf("%q resolved to %d:%d, wanted %d:%d", owner, uid, gid, want[0], want[1])
}
}
if _, _, err := idsOf("no-such-user-exists-here"); err == nil {
t.Error("a name this machine does not know was accepted")
}
if _, _, err := idsOf("root:something"); err == nil {
t.Error("a name with a colon was accepted; a name stands alone")
}
if uid, gid, err := idsOf("root"); err != nil || uid != 0 || gid != 0 {
t.Errorf("root resolved to %d:%d (%v); names must still work", uid, gid, err)
}
}