A login the mesh set is given back, and undeclaring one no longer stops the apply (hq issue 225)
A user had no removal, so an undeclared one failed as an orphan and aborted every apply after. Removal now keeps the account, gives back the shell recorded when the mesh first changed it if it is still the mesh's and still usable, and says why otherwise (hq ADR 0176 §2). A shell is refused before it is set unless it is executable and listed in /etc/shells, since usermod succeeds on a missing one.
This commit is contained in:
+106
-18
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
)
|
||||
|
||||
@@ -23,14 +24,35 @@ import (
|
||||
//
|
||||
// Reconciling, like everything else here: it is not told whether the user is new. Creating,
|
||||
// setting a shell and adding groups are each done only when the machine does not already agree.
|
||||
func applyUser(ctx context.Context, sys system.System, r *declaration.User, run Runner) (Outcome, error) {
|
||||
//
|
||||
// previous is this resource's record, which carries the shell the account had before the mesh
|
||||
// first changed it, so removal can give it back (novox/hq ADR 0176 §2, issue 225).
|
||||
func applyUser(ctx context.Context, sys system.System, r *declaration.User, run Runner,
|
||||
previous store.Applied) (Outcome, error) {
|
||||
out := begin(r)
|
||||
out.Action = "unchanged"
|
||||
// What was found is carried from the record for as long as the resource is recorded — for this
|
||||
// account only: a declaration that renamed its user says nothing about the new one's shell.
|
||||
if previous.Shell != nil && previous.Target == r.Name {
|
||||
kept := *previous.Shell
|
||||
out.shell = &kept
|
||||
}
|
||||
|
||||
login, exists, err := system.LookUpUser(ctx, system.Runner(run), r.Name)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
// **A shell is refused before anything is touched** (novox/hq issue 225). Refused after the
|
||||
// account was created or its groups changed, the account would be half the declaration's; a
|
||||
// refusal fails this resource and leaves the account exactly as it was.
|
||||
if r.Shell != "" && (!exists || login.Shell != r.Shell) {
|
||||
if err := system.UsableShell(r.Shell); err != nil {
|
||||
return out, fmt.Errorf("%q's shell was not set, and the account was left as it is: %w",
|
||||
r.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := sys.CreateUser(ctx, system.Runner(run), r.Name, r.Home, r.Shell); err != nil {
|
||||
return out, err
|
||||
@@ -46,26 +68,15 @@ func applyUser(ctx context.Context, sys system.System, r *declaration.User, run
|
||||
r.Name)
|
||||
}
|
||||
out.Action = "created"
|
||||
}
|
||||
|
||||
// The shell, only when it differs. Absent means the host asserts nothing — a field that
|
||||
// always asserts cannot express "leave it alone", which is the difference between managing a
|
||||
// machine and taking it over.
|
||||
if r.Shell != "" && login.Shell != r.Shell {
|
||||
if err := sys.SetUserShell(ctx, system.Runner(run), r.Name, r.Shell); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if back, _, err := system.LookUpUser(ctx, system.Runner(run), r.Name); err != nil {
|
||||
return out, err
|
||||
} else if back.Shell != r.Shell {
|
||||
return out, fmt.Errorf("set %q's shell to %q and the user database says %q",
|
||||
r.Name, r.Shell, back.Shell)
|
||||
}
|
||||
if out.Action == "unchanged" {
|
||||
out.Action = "updated"
|
||||
if r.Shell != "" {
|
||||
// No shell from before to give back: the account had none until the mesh made it.
|
||||
out.shell = &store.LoginShell{Set: login.Shell, Created: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Groups before the shell, so that a failure here comes before the shell is changed: a record
|
||||
// is written only for an apply that worked, and a shell changed by a failed one would be read
|
||||
// next time as the account's own, and the one it replaced lost.
|
||||
if len(r.Groups) > 0 {
|
||||
in, err := system.GroupsOf(ctx, system.Runner(run), r.Name)
|
||||
if err != nil {
|
||||
@@ -87,9 +98,86 @@ func applyUser(ctx context.Context, sys system.System, r *declaration.User, run
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The shell, only when it differs. Absent means the host asserts nothing — a field that
|
||||
// always asserts cannot express "leave it alone", which is the difference between managing a
|
||||
// machine and taking it over.
|
||||
if r.Shell != "" && login.Shell != r.Shell {
|
||||
if err := sys.SetUserShell(ctx, system.Runner(run), r.Name, r.Shell); err != nil {
|
||||
return out, err
|
||||
}
|
||||
if back, _, err := system.LookUpUser(ctx, system.Runner(run), r.Name); err != nil {
|
||||
return out, err
|
||||
} else if back.Shell != r.Shell {
|
||||
return out, fmt.Errorf("set %q's shell to %q and the user database says %q",
|
||||
r.Name, r.Shell, back.Shell)
|
||||
}
|
||||
// **What was found is recorded once** (novox/hq ADR 0176 §2). A later change keeps it: what
|
||||
// is given back is the shell from before the mesh, never the mesh's own earlier choice.
|
||||
if out.shell == nil {
|
||||
out.shell = &store.LoginShell{Found: login.Shell}
|
||||
}
|
||||
out.shell.Set = r.Shell
|
||||
if out.Action == "unchanged" {
|
||||
out.Action = "updated"
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// removeUser is what undeclaring a login does: never deleting the account, and giving back the
|
||||
// shell the mesh replaced when that is still safe (novox/hq ADR 0176 §2, issue 225).
|
||||
//
|
||||
// **The account is never deleted, whether or not the mesh created it.** An account owns a home,
|
||||
// files, a crontab, a mailbox — what a person did with it is not the mesh's to know, and deleting
|
||||
// it is the data loss ADR 0030 exists to prevent. It is the package's rule, on a login: the
|
||||
// mesh no longer requires it, which is not the same as "remove it".
|
||||
//
|
||||
// The shell goes back only while the account still has the one the mesh set — one a person chose
|
||||
// since is theirs — and only to a shell that is still usable: giving back a shell that has been
|
||||
// uninstalled since would break the very logins the giving back is for. Otherwise it is left, and
|
||||
// the outcome says why. Never errNoRemoval: an orphaned login that failed removal stopped the
|
||||
// whole apply, on every apply after.
|
||||
func removeUser(ctx context.Context, sys system.System, a store.Applied, run Runner) (string, string, error) {
|
||||
const kept = "the account is kept; the host never deletes a login"
|
||||
login, exists, err := system.LookUpUser(ctx, system.Runner(run), a.Target)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !exists {
|
||||
return "forgotten", "no longer there", nil
|
||||
}
|
||||
found := a.Shell
|
||||
switch {
|
||||
case found == nil:
|
||||
return "forgotten", kept + ", and its shell was never changed by the mesh", nil
|
||||
case found.Created:
|
||||
return "forgotten", kept + "; the mesh created it, so there is no shell from before to give back", nil
|
||||
case login.Shell != found.Set:
|
||||
return "forgotten", fmt.Sprintf("%s, and its shell %s left as it is: changed since the mesh set %s",
|
||||
kept, login.Shell, found.Set), nil
|
||||
case found.Found == "":
|
||||
return "forgotten", kept + ", and its shell left as it is: it had none before the mesh set one", nil
|
||||
}
|
||||
if err := system.UsableShell(found.Found); err != nil {
|
||||
return "forgotten", fmt.Sprintf("%s, and its shell %s left as it is: the one it had before "+
|
||||
"cannot be given back: %v", kept, login.Shell, err), nil
|
||||
}
|
||||
// A give-back that fails is said and not fatal: fatal, the record would stay and fail the same
|
||||
// way on every apply after — the very wedge this removal exists to end.
|
||||
if err := sys.SetUserShell(ctx, system.Runner(run), a.Target, found.Found); err != nil {
|
||||
return "forgotten", fmt.Sprintf("%s, and the shell it had before the mesh, %s, could not be "+
|
||||
"given back: %v", kept, found.Found, err), nil
|
||||
}
|
||||
if back, _, err := system.LookUpUser(ctx, system.Runner(run), a.Target); err != nil {
|
||||
return "", "", err
|
||||
} else if back.Shell != found.Found {
|
||||
return "forgotten", fmt.Sprintf("%s; gave back the shell %s and the user database says %s",
|
||||
kept, found.Found, back.Shell), nil
|
||||
}
|
||||
return "restored", fmt.Sprintf("%s; the shell it had before the mesh, %s, given back", kept, found.Found), nil
|
||||
}
|
||||
|
||||
// own sets a path's owner, when one was declared.
|
||||
//
|
||||
// Looked up by name every time rather than cached: a user's numeric id is not stable across
|
||||
|
||||
Reference in New Issue
Block a user