mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/module-groups delivering: 0 of 2 delivered
mesh/delivery delivered
A module puts the operator's account in a group by declaring the account with that group alone. The node-engine now records each group it added, takes back only those when nothing declared still asks for them, refuses a group the machine lacks before usermod runs, and states each such account as its module's resource of kind account: relogin needed while the running session lacks the group.
222 lines
7.2 KiB
Go
222 lines
7.2 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, 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] {
|
|
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.
|
|
func giveGroupsBack(ctx context.Context, sys system.System, a store.Applied, run Runner, wanted Wanted) (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, ", ")))
|
|
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
|
|
}
|