usermod --gid regroups every file under an account's home, and accounts were made with / as home: they now have /nonexistent, and an older account's home is moved there (without its files) before its group changes. A sub-path asked for later that holds data not in the group waits for the operator as the root would. The take act gives the group to hardlinked files an account owns (the *arr apps' downloads) and leaves root's; a file's setuid or setgid bit the regroup takes is never put back, so no setgid file hands out the library's group; each is counted. An apply no longer waits for a running take walk.
158 lines
6.4 KiB
Go
158 lines
6.4 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
|
|
}
|