Files
mesh-host/internal/system/identities.go
T
jschoubben 281837847e
mesh/merge-gate head 281837847e03 merged into trunk 303d4037e84e — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 281837847e03 merged into trunk 303d4037e84e — pass: its merge-check.sh passed
mesh/delivery delivered
Put busybox's account removal back, test the sample, and read a held lock as kept only for a running walk (hq ADR 0328 review)
An earlier change dropped alpine's RemoveAccount and RemoveGroup, so alpine made no
module identity at all. The sample beneath a root in shape had no test of its own.
And an apply that found the library's lock held read the library as kept even when
what held it was a take act about to be refused for a missing disk; it now does so
only while a take walk is running, and otherwise waits and judges as always.
2026-10-12 22:21:42 +02:00

174 lines
6.9 KiB
Go

package system
import (
"context"
"fmt"
"strconv"
"strings"
)
// A module's identity as a system account (novox/hq ADR 0328 §3, §4): an account and a group with exactly the numbers
// the controller allocated, no login shell, no home and no password. Optional beside System: a system that has
// none (android) makes no module identity, and the engine says so.
// CreateSystemGroup makes a system group with gid.
func (arch) CreateSystemGroup(ctx context.Context, run Runner, name string, gid int) error {
if _, err := run(ctx, "groupadd", "--system", "--gid", strconv.Itoa(gid), name); err != nil {
return fmt.Errorf("cannot create the group %q: %w", name, err)
}
return nil
}
// NoHome is the home field of every module account: a path that is not there, so nothing that follows an account's
// home — usermod regrouping every file under it, for one — ever walks a real tree. Never `/`: `usermod --gid` on
// an account whose home is `/` regroups every file it owns on the machine.
const NoHome = "/nonexistent"
// CreateSystemAccount makes a system account with uid and the group gid as its own: no home made, NoHome as the
// home field, nologin as its shell, and the password useradd leaves locked.
func (arch) CreateSystemAccount(ctx context.Context, run Runner, name string, uid, gid int, groups []string) error {
args := []string{"--system", "--uid", strconv.Itoa(uid), "--gid", strconv.Itoa(gid)}
if len(groups) > 0 {
args = append(args, "--groups", strings.Join(groups, ","))
}
args = append(args, "--no-create-home", "--home-dir", NoHome, "--shell", "/usr/bin/nologin", name)
if _, err := run(ctx, "useradd", args...); err != nil {
return fmt.Errorf("cannot create the account %q: %w", name, err)
}
return nil
}
// ModifyAccount gives an account the mesh made its primary group gid and exactly groups as its added groups.
//
// **Its home is moved to NoHome first, never its files.** `usermod --gid` regroups every file under the account's
// home that the account owns; an account made with `/` as its home (an earlier engine's) would have the whole
// machine walked as root. So an account whose home is not NoHome is given NoHome first, without -m: no file moves.
func (arch) ModifyAccount(ctx context.Context, run Runner, name string, gid int, groups []string) error {
passwd, err := run(ctx, "getent", "passwd", name)
if err != nil {
return fmt.Errorf("cannot read the account %q: %w", name, err)
}
fields := strings.Split(strings.TrimSpace(passwd), ":")
if len(fields) < 6 {
return fmt.Errorf("the user database gave %q for %q, which is not a passwd entry", strings.TrimSpace(passwd),
name)
}
if fields[5] != NoHome {
if _, err := run(ctx, "usermod", "--home", NoHome, name); err != nil {
return fmt.Errorf("cannot give the account %q no home before its groups: %w", name, err)
}
}
if _, err := run(ctx, "usermod", "--gid", strconv.Itoa(gid), "--groups", strings.Join(groups, ","),
name); err != nil {
return fmt.Errorf("cannot give the account %q its groups: %w", name, err)
}
return nil
}
// RemoveAccount removes an account, leaving every file it owns where it is.
func (arch) RemoveAccount(ctx context.Context, run Runner, name string) error {
if _, err := run(ctx, "userdel", name); err != nil {
return fmt.Errorf("cannot remove the account %q: %w", name, err)
}
return nil
}
// RemoveGroup removes a group.
func (arch) RemoveGroup(ctx context.Context, run Runner, name string) error {
if _, err := run(ctx, "groupdel", name); err != nil {
return fmt.Errorf("cannot remove the group %q: %w", name, err)
}
return nil
}
// CreateSystemGroup makes a system group with gid, with busybox's addgroup.
func (alpine) CreateSystemGroup(ctx context.Context, run Runner, name string, gid int) error {
if _, err := run(ctx, "addgroup", "-S", "-g", strconv.Itoa(gid), name); err != nil {
return fmt.Errorf("cannot create the group %q: %w", name, err)
}
return nil
}
// CreateSystemAccount makes a system account with busybox's adduser: -S system, -D no password, -H no home made.
func (alpine) CreateSystemAccount(ctx context.Context, run Runner, name string, uid, gid int, groups []string) error {
group, err := run(ctx, "getent", "group", strconv.Itoa(gid))
if err != nil {
return fmt.Errorf("cannot create the account %q: its group gid %d is not here: %w", name, gid, err)
}
groupName := name
if n, _, ok := strings.Cut(group, ":"); ok {
groupName = n
}
if _, err := run(ctx, "adduser", "-S", "-D", "-H", "-h", NoHome, "-s", "/sbin/nologin", "-u", strconv.Itoa(uid),
"-G", groupName, name); err != nil {
return fmt.Errorf("cannot create the account %q: %w", name, err)
}
for _, g := range groups {
if _, err := run(ctx, "addgroup", name, g); err != nil {
return fmt.Errorf("cannot put the account %q in %q: %w", name, g, err)
}
}
return nil
}
// ModifyAccount gives an account the mesh made exactly groups as its added groups, with busybox's addgroup and
// delgroup. Its primary group busybox cannot change (it has no usermod, and the mesh edits no passwd by hand):
// an account whose primary group must change is refused, saying so, and left as it is.
func (alpine) ModifyAccount(ctx context.Context, run Runner, name string, gid int, groups []string) error {
passwd, err := run(ctx, "getent", "passwd", name)
if err != nil {
return fmt.Errorf("cannot read the account %q: %w", name, err)
}
fields := strings.Split(strings.TrimSpace(passwd), ":")
if len(fields) < 4 || fields[3] != strconv.Itoa(gid) {
return fmt.Errorf("this system cannot change the account %q's primary group to %d: busybox has no usermod, "+
"and the mesh edits no passwd by hand: %w", name, gid, ErrUnsupported)
}
primary := ""
if g, err := run(ctx, "getent", "group", fields[3]); err == nil {
primary, _, _ = strings.Cut(strings.TrimSpace(g), ":")
}
out, err := run(ctx, "id", "-nG", name)
if err != nil {
return fmt.Errorf("cannot read the account %q's groups: %w", name, err)
}
have, want := map[string]bool{}, map[string]bool{}
for _, g := range strings.Fields(out) {
if g != primary && g != name {
have[g] = true
}
}
for _, g := range groups {
want[g] = true
if !have[g] {
if _, err := run(ctx, "addgroup", name, g); err != nil {
return fmt.Errorf("cannot put the account %q in %q: %w", name, g, err)
}
}
}
for g := range have {
if !want[g] {
if _, err := run(ctx, "delgroup", name, g); err != nil {
return fmt.Errorf("cannot take the account %q out of %q: %w", name, g, err)
}
}
}
return nil
}
// RemoveAccount removes an account with busybox's deluser.
func (alpine) RemoveAccount(ctx context.Context, run Runner, name string) error {
if _, err := run(ctx, "deluser", name); err != nil {
return fmt.Errorf("cannot remove the account %q: %w", name, err)
}
return nil
}
// RemoveGroup removes a group with busybox's delgroup.
func (alpine) RemoveGroup(ctx context.Context, run Runner, name string) error {
if _, err := run(ctx, "delgroup", name); err != nil {
return fmt.Errorf("cannot remove the group %q: %w", name, err)
}
return nil
}