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.
383 lines
15 KiB
Go
383 lines
15 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// Every module is given an identity of its own (novox/hq ADR 0328 §3, §4, §15, issue 567).
|
|
//
|
|
// The controller allocates each module a user and a group number once, from a range of its own, the same on every
|
|
// machine, and declares them as `mesh-<module>`. The engine makes the group and the account with exactly those
|
|
// numbers — a system account: no login shell, no home, no password — before anything of the declaration is
|
|
// applied, so a directory the module owns can be given to it and a container told to run as it.
|
|
//
|
|
// **Never root** (§11): a number that is 0, or a gid a root group has here (by its number and by its name), is
|
|
// refused for the whole declaration before anything is made or started, naming why — for an account and for an
|
|
// image alike. **Never another's**: a name that exists with other numbers, or numbers another name holds, is
|
|
// refused for that identity alone, naming whose, and its module is not attempted; the rest of the machine is.
|
|
//
|
|
// **What it made, and only that, goes** when the identity is no longer declared: the record says whether the
|
|
// engine made the account, the group or both, and an account or a group that was there before is never removed.
|
|
|
|
// accountsTool is how the engine reads and changes the machine's accounts: the system's own tools through the
|
|
// runner, which is what a test stands in for.
|
|
type accountsTool interface {
|
|
CreateSystemAccount(ctx context.Context, run system.Runner, name string, uid, gid int, groups []string) error
|
|
ModifyAccount(ctx context.Context, run system.Runner, name string, gid int, groups []string) error
|
|
CreateSystemGroup(ctx context.Context, run system.Runner, name string, gid int) error
|
|
RemoveAccount(ctx context.Context, run system.Runner, name string) error
|
|
RemoveGroup(ctx context.Context, run system.Runner, name string) error
|
|
}
|
|
|
|
// entry is one line the user or group database gives for a key: its name and its numbers.
|
|
type entry struct {
|
|
name string
|
|
id, prim int
|
|
}
|
|
|
|
// lookUp asks the database (passwd or group) for key, a name or a number. Absent is (entry{}, false, nil); a
|
|
// database that does not answer is an error, never read as absent (system.LookUpUser's rule).
|
|
func lookUp(ctx context.Context, run Runner, database, key string) (entry, bool, error) {
|
|
out, err := run(ctx, "getent", database, key)
|
|
if err != nil {
|
|
if code, ok := system.ExitCode(err); ok && code == 2 {
|
|
return entry{}, false, nil
|
|
}
|
|
return entry{}, false, fmt.Errorf("the %s database did not answer about %s: %w", database, key, err)
|
|
}
|
|
fields := strings.Split(strings.TrimSpace(strings.SplitN(out, "\n", 2)[0]), ":")
|
|
if len(fields) < 3 {
|
|
return entry{}, false, fmt.Errorf("the %s database gave %q for %s", database, strings.TrimSpace(out), key)
|
|
}
|
|
e := entry{name: fields[0], prim: -1}
|
|
if e.id, err = strconv.Atoi(fields[2]); err != nil {
|
|
return entry{}, false, fmt.Errorf("the %s database gave %q for %s", database, strings.TrimSpace(out), key)
|
|
}
|
|
if database == "passwd" && len(fields) >= 4 {
|
|
if e.prim, err = strconv.Atoi(fields[3]); err != nil {
|
|
e.prim = -1
|
|
}
|
|
}
|
|
return e, true, nil
|
|
}
|
|
|
|
// refuseRootIdentities is every identity and every container's run-as that is root's (§11), judged before
|
|
// anything of the declaration is applied: one is enough to refuse it whole, so nothing is made or started.
|
|
func refuseRootIdentities(d *declaration.Declaration) error {
|
|
var problems []string
|
|
for _, id := range d.Identities {
|
|
// Its user's number, its own group's (the same number), its primary group and every added group.
|
|
if err := rootIdentity(id.UID, id.UID); err != nil {
|
|
problems = append(problems, fmt.Sprintf("the identity %s: %v", id.Name, err))
|
|
}
|
|
for _, gid := range append([]int{id.GID}, id.Groups...) {
|
|
if why := rootGroup(gid); why != "" {
|
|
problems = append(problems, fmt.Sprintf("the identity %s is given gid %s, and a module's identity "+
|
|
"is never in a root group (novox/hq ADR 0328 §15)", id.Name, why))
|
|
}
|
|
}
|
|
}
|
|
for _, g := range d.Groups {
|
|
if why := rootGroup(g.GID); why != "" {
|
|
problems = append(problems, fmt.Sprintf("the library group %s is gid %s, and a library's group is never a "+
|
|
"root group (novox/hq ADR 0328 §15)", g.Name, why))
|
|
}
|
|
}
|
|
for _, r := range d.Resources {
|
|
if c, ok := r.(*declaration.Container); ok && c.RunAs != nil {
|
|
if err := rootIdentity(c.RunAs.UID, c.RunAs.GID); err != nil {
|
|
problems = append(problems, fmt.Sprintf("the container %s is told to run as uid %d, gid %d: %v",
|
|
c.ID, c.RunAs.UID, c.RunAs.GID, err))
|
|
}
|
|
}
|
|
}
|
|
if len(problems) == 0 {
|
|
return nil
|
|
}
|
|
sort.Strings(problems)
|
|
return fmt.Errorf("%s; nothing of this declaration was made or started", strings.Join(problems, "; "))
|
|
}
|
|
|
|
// applyIdentity makes one identity as declared (ADR 0328 §4): its own group `mesh-<module>` with the number of its
|
|
// uid, and its user with that uid, its primary group gid (its own, or its first library's) and its other libraries'
|
|
// groups as added groups — or refuses it naming whose the name or the numbers are. A user the engine made is given
|
|
// a changed primary or added groups; one that was there before is never changed, and is refused if it is not as
|
|
// declared. What it made is recorded on known; an identity already right is unchanged.
|
|
func applyIdentity(ctx context.Context, tool accountsTool, id declaration.Identity, names map[int]string,
|
|
known *store.State, run Runner) (Outcome, error) {
|
|
out := Outcome{ID: "identity " + id.Name, Type: "identity", Target: id.Name, Action: "unchanged"}
|
|
user, hasUser, err := lookUp(ctx, run, "passwd", id.Name)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
group, hasGroup, err := lookUp(ctx, run, "group", id.Name)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
byUID, uidUsed, err := lookUp(ctx, run, "passwd", strconv.Itoa(id.UID))
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
byGID, gidUsed, err := lookUp(ctx, run, "group", strconv.Itoa(id.UID))
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
rec := madeIdentity(known, id.Name)
|
|
const never = "; nothing was made, and nothing of the module was attempted (novox/hq ADR 0328 §4)"
|
|
switch {
|
|
case hasUser && user.id != id.UID:
|
|
return out, fmt.Errorf("the account %s is here with uid %d, and the mesh allocated uid %d%s", id.Name,
|
|
user.id, id.UID, never)
|
|
case hasUser && user.prim != id.GID && !rec.MadeUser:
|
|
return out, fmt.Errorf("the account %s is here with group %d, was not made by the mesh, and the mesh "+
|
|
"gives it group %d%s", id.Name, user.prim, id.GID, never)
|
|
case hasGroup && group.id != id.UID:
|
|
return out, fmt.Errorf("the group %s is here with gid %d, and a module's own group has its uid, %d%s",
|
|
id.Name, group.id, id.UID, never)
|
|
case uidUsed && byUID.name != id.Name:
|
|
return out, fmt.Errorf("uid %d is the account %s's here, and the mesh allocated it to %s%s", id.UID,
|
|
byUID.name, id.Name, never)
|
|
case gidUsed && byGID.name != id.Name:
|
|
return out, fmt.Errorf("gid %d is the group %s's here, and the mesh allocated it to %s%s", id.UID,
|
|
byGID.name, id.Name, never)
|
|
}
|
|
added := make([]string, 0, len(id.Groups))
|
|
for _, gid := range id.Groups {
|
|
added = append(added, names[gid])
|
|
}
|
|
sort.Strings(added)
|
|
rec.Module, rec.Name, rec.UID, rec.GID = id.Module, id.Name, id.UID, id.GID
|
|
var made []string
|
|
if !hasGroup {
|
|
if err := tool.CreateSystemGroup(ctx, run, id.Name, id.UID); err != nil {
|
|
return out, err
|
|
}
|
|
rec.MadeGroup = true
|
|
made = append(made, fmt.Sprintf("its own group (gid %d)", id.UID))
|
|
}
|
|
if !hasUser {
|
|
if err := tool.CreateSystemAccount(ctx, run, id.Name, id.UID, id.GID, added); err != nil {
|
|
recordIdentity(known, rec)
|
|
return out, err
|
|
}
|
|
rec.MadeUser = true
|
|
made = append(made, fmt.Sprintf("the account (uid %d, group %d%s), with no login, no home and no password",
|
|
id.UID, id.GID, alsoIn(added)))
|
|
} else if rec.MadeUser {
|
|
// The mesh's own account: its primary and added groups follow what is declared.
|
|
in, err := system.GroupsOf(ctx, system.Runner(run), id.Name)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
have := map[string]bool{}
|
|
for _, g := range in {
|
|
if g != id.Name && g != names[user.prim] {
|
|
have[g] = true
|
|
}
|
|
}
|
|
same := user.prim == id.GID && len(have) == len(added)
|
|
for _, g := range added {
|
|
same = same && have[g]
|
|
}
|
|
if !same {
|
|
if err := tool.ModifyAccount(ctx, run, id.Name, id.GID, added); err != nil {
|
|
return out, err
|
|
}
|
|
out.Action = "updated"
|
|
out.Detail = fmt.Sprintf("gave the account group %d%s", id.GID, alsoIn(added))
|
|
}
|
|
}
|
|
if rec.At.IsZero() {
|
|
rec.At = time.Now().UTC()
|
|
}
|
|
recordIdentity(known, rec)
|
|
if len(made) > 0 {
|
|
out.Action, out.Detail = "created", "made "+strings.Join(made, " and ")
|
|
} else if out.Detail == "" {
|
|
out.Detail = "here with its numbers"
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func alsoIn(groups []string) string {
|
|
if len(groups) == 0 {
|
|
return ""
|
|
}
|
|
return ", also in " + strings.Join(groups, ", ")
|
|
}
|
|
|
|
// applyGroup makes one library's group with the number the controller allocated (ADR 0328 §3), or refuses it naming
|
|
// whose the name or the number is, or that the number is a root group's (§15). What it made is recorded on known.
|
|
func applyGroup(ctx context.Context, tool accountsTool, g declaration.Group, known *store.State,
|
|
run Runner) (Outcome, error) {
|
|
out := Outcome{ID: "group " + g.Name, Type: "group", Target: g.Name, Action: "unchanged"}
|
|
if why := rootGroup(g.GID); why != "" {
|
|
return out, fmt.Errorf("its gid is %s, and a library's group is never a root group (novox/hq ADR 0328 §15); "+
|
|
"nothing was made", why)
|
|
}
|
|
byName, hasName, err := lookUp(ctx, run, "group", g.Name)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
byGID, gidUsed, err := lookUp(ctx, run, "group", strconv.Itoa(g.GID))
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
switch {
|
|
case hasName && byName.id != g.GID:
|
|
return out, fmt.Errorf("the group %s is here with gid %d, and the mesh allocated gid %d; nothing was made",
|
|
g.Name, byName.id, g.GID)
|
|
case gidUsed && byGID.name != g.Name:
|
|
return out, fmt.Errorf("gid %d is the group %s's here, and the mesh allocated it to %s; nothing was made",
|
|
g.GID, byGID.name, g.Name)
|
|
case hasName:
|
|
out.Detail = "here with its number"
|
|
return out, nil
|
|
}
|
|
if err := tool.CreateSystemGroup(ctx, run, g.Name, g.GID); err != nil {
|
|
return out, err
|
|
}
|
|
known.Groups = append(known.Groups, store.MadeGroup{Name: g.Name, GID: g.GID, At: time.Now().UTC()})
|
|
out.Action, out.Detail = "created", fmt.Sprintf("made the library's group (gid %d)", g.GID)
|
|
return out, nil
|
|
}
|
|
|
|
// removeGroups removes every library group the engine made that is no longer declared, while it still has the
|
|
// number the engine gave it. Never fatal: one that could not be removed (still a user's primary group) stays
|
|
// recorded, said, for the next apply.
|
|
func removeGroups(ctx context.Context, tool accountsTool, declared []declaration.Group, known *store.State,
|
|
run Runner) []Outcome {
|
|
wanted := map[string]bool{}
|
|
for _, g := range declared {
|
|
wanted[g.Name] = true
|
|
}
|
|
var out []Outcome
|
|
var keep []store.MadeGroup
|
|
for _, m := range known.Groups {
|
|
if wanted[m.Name] {
|
|
keep = append(keep, m)
|
|
continue
|
|
}
|
|
o := Outcome{ID: "group " + m.Name, Type: "group", Target: m.Name, Action: "removed",
|
|
Detail: "no longer declared: removed the group"}
|
|
g, has, err := lookUp(ctx, run, "group", m.Name)
|
|
switch {
|
|
case err != nil || (has && g.id != m.GID):
|
|
o.Action, o.Detail = "forgotten", "no longer declared: the group was left, it no longer has the gid the "+
|
|
"mesh gave it, or the database did not answer"
|
|
case has:
|
|
if err := tool.RemoveGroup(ctx, run, m.Name); err != nil {
|
|
o.Action, o.Detail = "waiting", "no longer declared: the group could not be removed: "+err.Error()
|
|
keep = append(keep, m)
|
|
}
|
|
default:
|
|
o.Action, o.Detail = "forgotten", "no longer declared, and no longer here"
|
|
}
|
|
out = append(out, o)
|
|
}
|
|
known.Groups = keep
|
|
return out
|
|
}
|
|
|
|
// madeIdentity is what the engine recorded of name, or an empty record.
|
|
func madeIdentity(known *store.State, name string) store.MadeIdentity {
|
|
for _, m := range known.Identities {
|
|
if m.Name == name {
|
|
return m
|
|
}
|
|
}
|
|
return store.MadeIdentity{}
|
|
}
|
|
|
|
// recordIdentity records what was made of one identity, in place of any record before.
|
|
func recordIdentity(known *store.State, rec store.MadeIdentity) {
|
|
for i, m := range known.Identities {
|
|
if m.Name == rec.Name {
|
|
known.Identities[i] = rec
|
|
return
|
|
}
|
|
}
|
|
if rec.MadeUser || rec.MadeGroup {
|
|
known.Identities = append(known.Identities, rec)
|
|
}
|
|
}
|
|
|
|
// removeIdentities removes what the engine made of every identity no longer declared — the account, then the
|
|
// group — and only while it still has the numbers the engine gave it. Never fatal: what could not be removed
|
|
// stays recorded, said, for the next apply.
|
|
func removeIdentities(ctx context.Context, tool accountsTool, declared []declaration.Identity, known *store.State,
|
|
run Runner) []Outcome {
|
|
wanted := map[string]bool{}
|
|
for _, id := range declared {
|
|
wanted[id.Name] = true
|
|
}
|
|
var out []Outcome
|
|
var keep []store.MadeIdentity
|
|
for _, m := range known.Identities {
|
|
if wanted[m.Name] {
|
|
keep = append(keep, m)
|
|
continue
|
|
}
|
|
o := Outcome{ID: "identity " + m.Name, Type: "identity", Target: m.Name, Action: "removed"}
|
|
var said []string
|
|
if m.MadeUser {
|
|
if u, has, err := lookUp(ctx, run, "passwd", m.Name); err != nil || (has && u.id != m.UID) {
|
|
said = append(said, "the account was left: it no longer has the uid the mesh gave it, or the "+
|
|
"database did not answer")
|
|
m.MadeUser = false
|
|
} else if has {
|
|
if err := tool.RemoveAccount(ctx, run, m.Name); err != nil {
|
|
said = append(said, "the account could not be removed: "+err.Error())
|
|
} else {
|
|
m.MadeUser = false
|
|
said = append(said, "removed the account")
|
|
}
|
|
} else {
|
|
m.MadeUser = false
|
|
}
|
|
}
|
|
if m.MadeGroup && !m.MadeUser {
|
|
if g, has, err := lookUp(ctx, run, "group", m.Name); err != nil || (has && g.id != m.GID) {
|
|
said = append(said, "the group was left: it no longer has the gid the mesh gave it, or the "+
|
|
"database did not answer")
|
|
m.MadeGroup = false
|
|
} else if has {
|
|
if err := tool.RemoveGroup(ctx, run, m.Name); err != nil {
|
|
said = append(said, "the group could not be removed: "+err.Error())
|
|
} else {
|
|
m.MadeGroup = false
|
|
said = append(said, "removed the group")
|
|
}
|
|
} else {
|
|
m.MadeGroup = false
|
|
}
|
|
}
|
|
if m.MadeUser || m.MadeGroup {
|
|
o.Action = "waiting"
|
|
keep = append(keep, m)
|
|
}
|
|
o.Detail = "no longer declared: " + strings.Join(said, "; ")
|
|
out = append(out, o)
|
|
}
|
|
known.Identities = keep
|
|
return out
|
|
}
|
|
|
|
// toolFor is the accounts tool of a system, or why it has none.
|
|
func toolFor(sys system.System) (accountsTool, error) {
|
|
if t, ok := sys.(accountsTool); ok {
|
|
return t, nil
|
|
}
|
|
return nil, errors.New("this system makes no module identity: it has no way to make a system account")
|
|
}
|