mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/module-groups delivering: 0 of 2 delivered
mesh/delivery delivered
A module puts the operator's account in a group by declaring the account with that group alone. The node-engine now records each group it added, takes back only those when nothing declared still asks for them, refuses a group the machine lacks before usermod runs, and states each such account as its module's resource of kind account: relogin needed while the running session lacks the group.
104 lines
3.4 KiB
Go
104 lines
3.4 KiB
Go
package accounts
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Runner runs a command and answers what it printed — the apply's own (apply.ExecRunner).
|
|
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
|
|
|
// Exec reads the machine through its command lines and the process table. **Reads only**: `id`, `getent`,
|
|
// the machine's own manager's `systemctl show`, and a status file under /proc (a test holds it).
|
|
type Exec struct {
|
|
Run Runner
|
|
// Proc is where the process table is; empty is /proc. A test points it at a directory of its own.
|
|
Proc string
|
|
}
|
|
|
|
// InDatabase is `id -nG`: every group the user database lists the account in.
|
|
func (e Exec) InDatabase(ctx context.Context, account string) ([]string, error) {
|
|
out, err := e.Run(ctx, "id", "-nG", account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return strings.Fields(out), nil
|
|
}
|
|
|
|
// Session reads the account's own manager, user@<uid>.service, from the machine's manager — never from
|
|
// the account's, which asking would start — and the groups its process holds, from its status file.
|
|
func (e Exec) Session(ctx context.Context, account string, groups []string) (Session, error) {
|
|
passwd, err := e.Run(ctx, "getent", "passwd", account)
|
|
if err != nil {
|
|
return Session{}, fmt.Errorf("the user database did not answer about %q: %w", account, err)
|
|
}
|
|
fields := strings.Split(strings.TrimSpace(passwd), ":")
|
|
if len(fields) < 7 || fields[2] == "" {
|
|
return Session{}, fmt.Errorf("the user database gave no number for %q", account)
|
|
}
|
|
shown, err := e.Run(ctx, "systemctl", "show", "--property=MainPID", "--value", "user@"+fields[2]+".service")
|
|
if err != nil {
|
|
return Session{}, fmt.Errorf("the machine's service manager did not say whether %q's own runs: %w", account, err)
|
|
}
|
|
pid := strings.TrimSpace(shown)
|
|
if pid == "" || pid == "0" {
|
|
return Session{}, nil
|
|
}
|
|
held, err := e.heldBy(pid)
|
|
if err != nil {
|
|
return Session{}, err
|
|
}
|
|
s := Session{Running: true, Has: map[string]bool{}}
|
|
for _, g := range groups {
|
|
entry, err := e.Run(ctx, "getent", "group", g)
|
|
if err != nil {
|
|
return Session{}, fmt.Errorf("the group database did not answer about %q: %w", g, err)
|
|
}
|
|
parts := strings.Split(strings.TrimSpace(entry), ":")
|
|
if len(parts) < 3 {
|
|
return Session{}, fmt.Errorf("the group database gave %q for %q, which is not a group entry", entry, g)
|
|
}
|
|
s.Has[g] = held[parts[2]]
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// heldBy is every group id a process holds, from the Groups line of its status file.
|
|
func (e Exec) heldBy(pid string) (map[string]bool, error) {
|
|
proc := e.Proc
|
|
if proc == "" {
|
|
proc = "/proc"
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(proc, pid, "status"))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, fmt.Errorf("the account's manager, process %s, ended while it was read", pid)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Its supplementary groups, and its own group, which the supplementary list need not repeat.
|
|
held := map[string]bool{}
|
|
named := false
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
if rest, ok := strings.CutPrefix(line, "Groups:"); ok {
|
|
named = true
|
|
for _, gid := range strings.Fields(rest) {
|
|
held[gid] = true
|
|
}
|
|
}
|
|
if rest, ok := strings.CutPrefix(line, "Gid:"); ok {
|
|
if f := strings.Fields(rest); len(f) > 0 {
|
|
held[f[0]] = true
|
|
}
|
|
}
|
|
}
|
|
if !named {
|
|
return nil, fmt.Errorf("process %s's status names no groups", pid)
|
|
}
|
|
return held, nil
|
|
}
|