The engine is built without cgo, so a check by name missed a root group a name service holds or one number carrying another name: the root groups are resolved to numbers (now with kmem and shadow) and a number named nothing is refused for containers too. A group without write is granted nothing (§2). The look reads each path off the lock within five seconds, so a dead network mount cannot stop every health statement. A grant's account may not start with a dash. A group a record leaves for another asker is handed to it, so it is still taken back when the last asker goes (§5). The scheduler skips paths issue 339's guard refuses.
231 lines
7.6 KiB
Go
231 lines
7.6 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// An account's groups, from any module (novox/hq ADR 0252, issue 247).
|
|
//
|
|
// **A module that needs the account in a group declares the account with that group, and nothing else
|
|
// of it.** The shell's module sets the account's shell; the lighting daemon's module puts the same
|
|
// account in `openrazer`; the container runtime's in `docker`. A shell is one value and stays one
|
|
// module's (the controller refuses two); a group is added, so several modules' groups never contradict.
|
|
//
|
|
// **The mesh takes back only what it gave.** A group the account was in before is the machine's or the
|
|
// operator's, and stays when every resource that named it goes. A group the mesh put the account in is
|
|
// recorded on the resource that put it there, and given back when that resource stops asking for it —
|
|
// unless another declared resource still asks for the same group, which keeps it.
|
|
//
|
|
// **A group takes effect at the next login.** The machine's group database changes at once; every
|
|
// process already running, the account's own service manager and everything it started included, keeps
|
|
// the groups it started with. So the outcome says a new login is needed, and the node-engine's account
|
|
// judge (internal/accounts) says so on every look until the account's running manager has the group.
|
|
|
|
// Wanted is every group a declaration asks an account to be in, and which resources ask: account →
|
|
// group → resource ids.
|
|
type Wanted map[string]map[string][]string
|
|
|
|
// groupsWanted reads every user resource's groups from a declaration.
|
|
func groupsWanted(resources []declaration.Resource) Wanted {
|
|
w := Wanted{}
|
|
for _, r := range resources {
|
|
u, ok := r.(*declaration.User)
|
|
if !ok || u.Name == "" {
|
|
continue
|
|
}
|
|
for _, g := range u.Groups {
|
|
if w[u.Name] == nil {
|
|
w[u.Name] = map[string][]string{}
|
|
}
|
|
w[u.Name][g] = append(w[u.Name][g], u.ID)
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
// othersAsk is every resource other than one that asks for an account to be in a group.
|
|
func (w Wanted) othersAsk(account, group, except string) []string {
|
|
var others []string
|
|
for _, id := range w[account][group] {
|
|
if id != except {
|
|
others = append(others, id)
|
|
}
|
|
}
|
|
return others
|
|
}
|
|
|
|
// applyGroups makes the account be in every group the resource declares, and takes it out of every group
|
|
// this resource put it in and no longer asks for. What the mesh put the account in is recorded on out.
|
|
func applyGroups(ctx context.Context, sys system.System, r *declaration.User, run Runner,
|
|
previous store.Applied, wanted Wanted, handed Handed, out *Outcome) error {
|
|
// What this resource put the account in before, for this account only: a declaration that renamed its
|
|
// user says nothing about the new one's groups.
|
|
var added []string
|
|
if previous.Target == r.Name {
|
|
added = append(added, previous.Groups...)
|
|
}
|
|
if len(r.Groups) == 0 && len(added) == 0 {
|
|
return nil
|
|
}
|
|
in, err := system.GroupsOf(ctx, system.Runner(run), r.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
already := setOf(in)
|
|
declared := setOf(r.Groups)
|
|
|
|
var put []string
|
|
for _, want := range r.Groups {
|
|
if already[want] {
|
|
// A record that went handed it here: the mesh's, and this resource's to take back (novox/hq ADR 0312 §5).
|
|
if handed.has(r.Name, want) && !contains(added, want) {
|
|
added = append(added, want)
|
|
}
|
|
continue
|
|
}
|
|
exists, err := system.GroupExists(ctx, system.Runner(run), want)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return fmt.Errorf("%q was not put in the group %q: this machine has no such group yet. The package "+
|
|
"that makes it is not installed, or is declared after the account", r.Name, want)
|
|
}
|
|
if err := sys.AddUserToGroup(ctx, system.Runner(run), r.Name, want); err != nil {
|
|
return err
|
|
}
|
|
already[want] = true
|
|
put = append(put, want)
|
|
if !contains(added, want) {
|
|
added = append(added, want)
|
|
}
|
|
}
|
|
|
|
// What this resource put the account in and no longer asks for.
|
|
var kept, said []string
|
|
for _, g := range added {
|
|
switch {
|
|
case declared[g]:
|
|
kept = append(kept, g)
|
|
case !already[g]:
|
|
// Taken out since, by a person: nothing to give back.
|
|
case len(wanted.othersAsk(r.Name, g, r.ID)) > 0:
|
|
said = append(said, fmt.Sprintf("left in %s, which %s still asks for", g,
|
|
strings.Join(wanted.othersAsk(r.Name, g, r.ID), ", ")))
|
|
default:
|
|
gone, why := leaveGroup(ctx, sys, r.Name, g, run)
|
|
if !gone {
|
|
kept = append(kept, g)
|
|
}
|
|
said = append(said, why)
|
|
}
|
|
}
|
|
out.groups = kept
|
|
if len(put) > 0 {
|
|
said = append([]string{fmt.Sprintf("put in %s; a session that began before has %s only after a new "+
|
|
"login", strings.Join(put, ", "), them(len(put)))}, said...)
|
|
}
|
|
if len(put) > 0 || len(said) > 0 {
|
|
if out.Action == "unchanged" {
|
|
out.Action = "updated"
|
|
}
|
|
out.Detail = joinDetail(out.Detail, strings.Join(said, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// giveGroupsBack is removeUser's groups: every group the mesh put the account in, taken back unless
|
|
// another declared resource still asks for it. Never fatal, for the shell's reason: a removal that failed
|
|
// would stay recorded and fail the same way on every apply after. Empty when there was nothing to say.
|
|
//
|
|
// A group left because another declared resource still asks for it is handed to that resource (handed), which
|
|
// records it as the mesh's, so it is taken back once nobody asks (novox/hq ADR 0312 §5).
|
|
func giveGroupsBack(ctx context.Context, sys system.System, a store.Applied, run Runner, wanted Wanted,
|
|
handed Handed) (bool, string) {
|
|
if len(a.Groups) == 0 {
|
|
return false, ""
|
|
}
|
|
in, err := system.GroupsOf(ctx, system.Runner(run), a.Target)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("the groups the mesh put it in (%s) were not given back: %v",
|
|
strings.Join(a.Groups, ", "), err)
|
|
}
|
|
already := setOf(in)
|
|
gave := false
|
|
var said []string
|
|
for _, g := range a.Groups {
|
|
if !already[g] {
|
|
continue
|
|
}
|
|
if others := wanted.othersAsk(a.Target, g, a.ID); len(others) > 0 {
|
|
said = append(said, fmt.Sprintf("left in %s, which %s still asks for", g, strings.Join(others, ", ")))
|
|
handed.hand(a.Target, g)
|
|
continue
|
|
}
|
|
gone, why := leaveGroup(ctx, sys, a.Target, g, run)
|
|
gave = gave || gone
|
|
said = append(said, why)
|
|
}
|
|
return gave, strings.Join(said, "; ")
|
|
}
|
|
|
|
// leaveGroup takes an account out of one group the mesh put it in, read back from the machine, and says
|
|
// what came of it.
|
|
func leaveGroup(ctx context.Context, sys system.System, account, group string, run Runner) (bool, string) {
|
|
l, ok := sys.(system.GroupLeaver)
|
|
if !ok {
|
|
return false, fmt.Sprintf("left in %s: this machine's system cannot take an account out of one group", group)
|
|
}
|
|
if err := l.RemoveUserFromGroup(ctx, system.Runner(run), account, group); err != nil {
|
|
return false, fmt.Sprintf("left in %s: %v", group, err)
|
|
}
|
|
in, err := system.GroupsOf(ctx, system.Runner(run), account)
|
|
if err != nil {
|
|
return false, fmt.Sprintf("taken out of %s, and the group database could not be read back: %v", group, err)
|
|
}
|
|
if setOf(in)[group] {
|
|
return false, fmt.Sprintf("taken out of %s, and the group database still lists it there", group)
|
|
}
|
|
return true, fmt.Sprintf("taken out of %s, which the mesh had put it in", group)
|
|
}
|
|
|
|
func setOf(items []string) map[string]bool {
|
|
s := map[string]bool{}
|
|
for _, i := range items {
|
|
s[i] = true
|
|
}
|
|
return s
|
|
}
|
|
|
|
func contains(items []string, want string) bool {
|
|
for _, i := range items {
|
|
if i == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func them(n int) string {
|
|
if n == 1 {
|
|
return "it"
|
|
}
|
|
return "them"
|
|
}
|
|
|
|
func joinDetail(a, b string) string {
|
|
switch {
|
|
case a == "":
|
|
return b
|
|
case b == "":
|
|
return a
|
|
}
|
|
return a + "; " + b
|
|
}
|