Give back only the groups the mesh added, and say when a new login is needed (hq ADR 0251, issue 247)
mesh/delivery-group group feat/module-groups checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
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 checking: 0 of 2 member(s) ready
mesh/delivery superseded: a newer head of the same pull request
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
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.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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 0251, 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
|
||||
}
|
||||
Reference in New Issue
Block a user