Files
mesh-host/internal/accounts/exec.go
T
jochen ab4ca44f98
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
Give back only the groups the mesh added, and say when a new login is needed (hq ADR 0252, issue 247)
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.
2026-10-08 12:04:08 +02:00

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
}