126 lines
5.7 KiB
Go
126 lines
5.7 KiB
Go
package apply
|
|
|
|
// A mesh directory outside a home is given its mode and owner only through a descriptor (novox/hq issue 358).
|
|
//
|
|
// Below a home every component is opened without following a link (ADR 0266, home_links.go), and a directory the
|
|
// operator handed to the mesh is changed through one descriptor opened from / with no link followed (issue 356).
|
|
// Every other directory the mesh owns — a module's state under /var/lib, a drop-in directory under /etc — was
|
|
// given its mode and owner by path, with chmod and chown, which follow a link anywhere in the path: whoever could
|
|
// turn a parent into a link between two applies directed root's chmod and chown at the link's target.
|
|
//
|
|
// The danger is a link another account can make or replace, not a link as such: /var/run is a link to /run on
|
|
// every node, and a disk is sometimes reached through one. So (the decision recorded on issue 358, 2026-10-11):
|
|
//
|
|
// - **the directory is opened component by component from /**, each with O_DIRECTORY|O_NOFOLLOW, and its mode
|
|
// and owner are changed with fchmod and fchown on that descriptor; a directory the walk makes is made in its
|
|
// parent's descriptor. A link put in place after any check is refused by the kernel at the moment of use;
|
|
// - **a link in a parent is followed only when root alone could have put it there**: the link is root's, and
|
|
// every directory from / down to the one holding it is root's and writable by no other account (no write for
|
|
// others, no write for a group other than root's). Its target is then walked the same way, from /;
|
|
// - **any other link refuses that directory**, naming the link and the account that could replace it; nothing
|
|
// is changed there, and the rest of the declaration applies;
|
|
// - **the directory itself is never a link** (issue 339, applyDirectory).
|
|
//
|
|
// A directory below a home keeps the walk of ADR 0266, which follows no link below the home at all.
|
|
|
|
import (
|
|
"fmt"
|
|
osuser "os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
)
|
|
|
|
// LinkInPathError is a mesh directory refused because a link in its path is one another account could replace.
|
|
type LinkInPathError struct {
|
|
// Path is the directory declared, Link the link in its path, Who the account that could replace it.
|
|
Path, Link, Who string
|
|
}
|
|
|
|
func (e *LinkInPathError) Error() string { return e.Path + " " + e.why() }
|
|
|
|
// why is the refusal without the path it refuses, as a PlacementRefusedError says it.
|
|
func (e *LinkInPathError) why() string {
|
|
return fmt.Sprintf("is reached through the link %s, which %s could replace: the node-engine runs as root and "+
|
|
"follows a link to a directory it gives a mode and owner only when root alone could have made it (novox/hq "+
|
|
"issue 358). Make %s a directory, or make it and every directory above it root's and writable by no other "+
|
|
"account", e.Link, e.Who, e.Link)
|
|
}
|
|
|
|
// couldReplace says who besides root could replace an entry of a directory with this owner, group and mode: ""
|
|
// when nobody can. A variable so a test, which runs below a directory every account may write (/tmp), can name
|
|
// which of its own directories count as root's alone.
|
|
var couldReplace = func(path string, uid, gid, mode uint32) string {
|
|
if uid != 0 {
|
|
return "the account " + accountNamed(uid) + " (it owns " + path + ")"
|
|
}
|
|
if mode&0o002 != 0 {
|
|
return "every account (" + path + " is writable by all)"
|
|
}
|
|
if mode&0o020 != 0 && gid != 0 {
|
|
return "the group " + groupNamed(gid) + " (it may write " + path + ")"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// linkReplacer says who besides root owns a link: "" when root does. A variable for the same reason.
|
|
var linkReplacer = func(link string, uid uint32) string {
|
|
if uid != 0 {
|
|
return "the account " + accountNamed(uid) + " (it owns the link)"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func accountNamed(uid uint32) string {
|
|
if u, err := osuser.LookupId(strconv.FormatUint(uint64(uid), 10)); err == nil {
|
|
return u.Username
|
|
}
|
|
return "with uid " + strconv.FormatUint(uint64(uid), 10)
|
|
}
|
|
|
|
func groupNamed(gid uint32) string {
|
|
if g, err := osuser.LookupGroupId(strconv.FormatUint(uint64(gid), 10)); err == nil {
|
|
return g.Name
|
|
}
|
|
return "with gid " + strconv.FormatUint(uint64(gid), 10)
|
|
}
|
|
|
|
// JudgeDirectories is the walk's judgement over every directory a declaration names, read-only (novox/hq issue
|
|
// 358): nothing is made, chmodded or chowned. One line per directory, in the declaration's order, after one line
|
|
// that counts them: placed, would be refused and why, refused already by the placement guard (issue 339), or
|
|
// below a home, where ADR 0266's walk judges it as before.
|
|
func JudgeDirectories(d *declaration.Declaration) string {
|
|
var lines []string
|
|
var placed, refused, guarded, homed int
|
|
for _, r := range d.Resources {
|
|
dir, ok := r.(*declaration.Directory)
|
|
if !ok {
|
|
continue
|
|
}
|
|
path := filepath.Clean(dir.Path)
|
|
if err := refusePath(path, placingDirectory, moduleOfID(dir.ID), dir.Owner); err != nil {
|
|
guarded++
|
|
lines = append(lines, path+": refused already by the placement guard: "+err.Error())
|
|
continue
|
|
}
|
|
if home := homeAbove(path); home != "" {
|
|
homed++
|
|
lines = append(lines, path+": below the home "+home+", walked without following any link as before (ADR 0266)")
|
|
continue
|
|
}
|
|
if err := judgeMeshDir(path); err != nil {
|
|
refused++
|
|
lines = append(lines, path+": would be refused: "+err.Error())
|
|
continue
|
|
}
|
|
placed++
|
|
lines = append(lines, path+": placed")
|
|
}
|
|
head := fmt.Sprintf("%d directories declared: %d placed, %d would be refused, %d refused already by the "+
|
|
"placement guard, %d below a home (novox/hq issue 358; judged read-only, nothing changed)",
|
|
placed+refused+guarded+homed, placed, refused, guarded, homed)
|
|
return strings.Join(append([]string{head}, lines...), "\n")
|
|
}
|