Unify trunk on main: initialization → main #3
+47
-13
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user