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-`. 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-` 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") }