ADR 0328 supersedes ADR 0325: shared data is a library with one home, kept usable by its group rather than by ACL entries. The engine now makes each library's group and each module's user in its libraries' groups; keeps a library's root and requested sub-paths in its group with group write and setgid on its home, making missing sub-paths as the root is; waits for the operator's word before touching a root that holds data, and on the take act walks it by descriptor (no link, same filesystem, bounded and resumed, files with other names left); holds the containers of a library whose disk or mount is not there; and puts the tool runner's account in the group. The ACL keeping, mask repair and watch are gone; an access is again as before ADR 0325.
148 lines
4.6 KiB
Go
148 lines
4.6 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/accounts"
|
|
)
|
|
|
|
// Root's numbers, which no module identity is and whose data is never kept (novox/hq ADR 0328 §15).
|
|
//
|
|
// A module identity whose uid or gid is 0, or whose gid is a group whose members act as root (accounts.RootGroups,
|
|
// the account judge's own closed list), would be a way to root through a container or a shared folder: refused,
|
|
// naming why. A shared folder owned by root or such a group is the machine's own data, never kept.
|
|
|
|
// RootGroupIDs is the number of every group whose members act as root (accounts.RootGroups), as this machine
|
|
// numbers them: read from /etc/group itself and from the machine's name service (`getent group`), since the engine
|
|
// is built without cgo and Go's own lookup reads /etc/group alone — a group a name service holds, or one number
|
|
// carrying several names, would otherwise pass a check made by name. Read at most once a minute, every look and
|
|
// apply judging against the same answer, and the name service given five seconds in all. A variable so a test can
|
|
// stand in.
|
|
var RootGroupIDs = func() map[int]string {
|
|
rootIDs.Lock()
|
|
defer rootIDs.Unlock()
|
|
if rootIDs.ids == nil || time.Since(rootIDs.at) > time.Minute {
|
|
rootIDs.ids, rootIDs.at = readRootGroupIDs(), time.Now()
|
|
}
|
|
return rootIDs.ids
|
|
}
|
|
|
|
var rootIDs struct {
|
|
sync.Mutex
|
|
ids map[int]string
|
|
at time.Time
|
|
}
|
|
|
|
func readRootGroupIDs() map[int]string {
|
|
out := map[int]string{}
|
|
if raw, err := os.ReadFile("/etc/group"); err == nil {
|
|
for name, gid := range groupEntries(string(raw)) {
|
|
if slices.Contains(accounts.RootGroups, name) {
|
|
out[gid] = name
|
|
}
|
|
}
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
for _, name := range accounts.RootGroups {
|
|
raw, err := exec.CommandContext(ctx, "getent", "group", name).Output()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for found, gid := range groupEntries(string(raw)) {
|
|
if found == name {
|
|
out[gid] = name
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// groupEntries reads group database lines, name:password:gid:members, as name to number.
|
|
func groupEntries(raw string) map[string]int {
|
|
out := map[string]int{}
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
fields := strings.Split(strings.TrimSpace(line), ":")
|
|
if len(fields) < 3 || strings.HasPrefix(fields[0], "#") {
|
|
continue
|
|
}
|
|
if gid, err := strconv.Atoi(fields[2]); err == nil {
|
|
out[fields[0]] = gid
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GroupName is the name this machine's group database gives a number, empty when none. A variable so a test can
|
|
// stand in for groups it cannot make.
|
|
var GroupName = func(gid int) string {
|
|
if g, err := user.LookupGroupId(strconv.Itoa(gid)); err == nil {
|
|
return g.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// rootGroup is why gid is a root group here — 0, a number a root group has, or a number named as one — or "".
|
|
func rootGroup(gid int) string {
|
|
if gid == 0 {
|
|
return "group 0"
|
|
}
|
|
if name, is := RootGroupIDs()[gid]; is {
|
|
return fmt.Sprintf("gid %d, which is %s here, whose members act as root", gid, name)
|
|
}
|
|
if name := GroupName(gid); slices.Contains(accounts.RootGroups, name) {
|
|
return fmt.Sprintf("gid %d, which is %s here, whose members act as root", gid, name)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// rootOwned is why a folder owned by uid and gid is root's data (ADR 0325 §12, kept for the legacy reader), or "": owned by root, or by a
|
|
// root group.
|
|
func rootOwned(uid, gid uint32) string {
|
|
if uid == 0 {
|
|
return "owned by root"
|
|
}
|
|
if why := rootGroup(int(gid)); why != "" {
|
|
return "owned by " + why
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// rootIdentity is why a module identity's numbers are refused (ADR 0328 §15), or nil.
|
|
func rootIdentity(uid, gid int) error {
|
|
if uid <= 0 {
|
|
return fmt.Errorf("its uid is %d, and a module's identity is never root (novox/hq ADR 0328 §15)", uid)
|
|
}
|
|
if why := rootGroup(gid); why != "" {
|
|
return fmt.Errorf("its gid is %s, and a module's identity is never in a root group (novox/hq ADR 0328 §15)", why)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handed is every group a record that went in this apply put an account in, and another declared resource still
|
|
// asks for: account → group. The resource that still asks records it as the mesh's when it is applied, so the group
|
|
// is taken back once nobody asks for it any more, rather than staying for ever because the record that put it
|
|
// there went first (novox/hq ADR 0252 §3).
|
|
type Handed map[string]map[string]bool
|
|
|
|
func (h Handed) hand(account, group string) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
if h[account] == nil {
|
|
h[account] = map[string]bool{}
|
|
}
|
|
h[account][group] = true
|
|
}
|
|
|
|
func (h Handed) has(account, group string) bool { return h[account][group] }
|