The mesh owns the operator's ~/.ssh: account fact + home-scoped resources (to-be 29)
A node carries its operator account (name + home; migration 0036, Node.Account,
SetAccount, 'node account' CLI). The account and its home are offered as
machine facts ${machine:account} / ${machine:account-home}, and machineInto
now resolves placeholders in a resource's path and owner (not just content), so
a module writes into a person's home naming what it cannot know. A RosterFile
gains Home: the file is placed under the account's home and chowned to it, its
template sees each node's Account, and a machine with no account gets none —
this is how the ssh Host blocks for every node reach a person's ~/.ssh. Roster
carries per-node accounts (Rendering.Accounts). Tested, including ssh-client
composed end-to-end. Not deployed.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
-- A node has an operator account: the human login on it (novox/hq to-be 29).
|
||||
--
|
||||
-- The mesh modelled the machine but not the person on it — `jochens` on novox, `ace` on ace,
|
||||
-- `jochen` on shanks and g14. That name decides who a file under a home is owned by and which
|
||||
-- account `ssh <node>` logs in as; it was silently lost when the predecessor's per-node `user:`
|
||||
-- was not carried over, and `ssh ace` failed to `ace` because nothing here said so.
|
||||
--
|
||||
-- Empty rather than null and defaulted, because "no operator account known yet" is a real state
|
||||
-- (a freshly enrolled machine, a headless box). The home is stored too rather than always assumed
|
||||
-- to be /home/<account>, because root's is /root and a machine may put a home elsewhere; empty
|
||||
-- means "derive it" (/root for root, /home/<account> otherwise), so the common case needs no entry.
|
||||
alter table node add column account text not null default '';
|
||||
alter table node add column account_home text not null default '';
|
||||
@@ -55,6 +55,29 @@ type Node struct {
|
||||
// AdoptedSince is when it last became so; zero for a converged node.
|
||||
Adopted bool
|
||||
AdoptedSince time.Time
|
||||
|
||||
// Account is the operator's login on this machine — `jochens` on novox, `ace` on ace (novox/hq
|
||||
// to-be 29). Empty when none is known yet. AccountHome is where that account's home is; empty
|
||||
// means derive it (/root for root, /home/<account> otherwise), so the common case needs no
|
||||
// entry. What decides who a file under a home is owned by, and which account `ssh <node>` uses.
|
||||
Account string
|
||||
AccountHome string
|
||||
}
|
||||
|
||||
// Home is the account's home directory, derived when not stored: /root for root, /home/<account>
|
||||
// otherwise. Empty only when there is no account at all.
|
||||
func (n Node) Home() string {
|
||||
if n.AccountHome != "" {
|
||||
return n.AccountHome
|
||||
}
|
||||
switch n.Account {
|
||||
case "":
|
||||
return ""
|
||||
case "root":
|
||||
return "/root"
|
||||
default:
|
||||
return "/home/" + n.Account
|
||||
}
|
||||
}
|
||||
|
||||
// Silent is how long since this node was last heard from, and whether it ever was.
|
||||
@@ -112,12 +135,13 @@ func (i *Inventory) AddNodeAs(ctx context.Context, name string, adopted bool) (N
|
||||
|
||||
// nodeColumns and scanNode are the one reading of a node row, so every way of finding a node
|
||||
// says whether it is adopted.
|
||||
const nodeColumns = `id, name, created, last_seen, adopted, adopted_since`
|
||||
const nodeColumns = `id, name, created, last_seen, adopted, adopted_since, account, account_home`
|
||||
|
||||
func scanNode(row pgx.Row) (Node, error) {
|
||||
var n Node
|
||||
var seen, since *time.Time
|
||||
if err := row.Scan(&n.ID, &n.Name, &n.Created, &seen, &n.Adopted, &since); err != nil {
|
||||
if err := row.Scan(&n.ID, &n.Name, &n.Created, &seen, &n.Adopted, &since,
|
||||
&n.Account, &n.AccountHome); err != nil {
|
||||
return Node{}, err
|
||||
}
|
||||
if seen != nil {
|
||||
@@ -129,6 +153,21 @@ func scanNode(row pgx.Row) (Node, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// SetAccount records the operator account on a node — its human login — and optionally where that
|
||||
// account's home is (novox/hq to-be 29). An empty home means the mesh derives it. Clearing the
|
||||
// account (empty name) is allowed: a machine may stop having a known operator.
|
||||
func (i *Inventory) SetAccount(ctx context.Context, node, account, home string) error {
|
||||
tag, err := i.store.Pool().Exec(ctx,
|
||||
`update node set account = $1, account_home = $2 where name = $3`, account, home, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("%w: %s", ErrNoSuchNode, node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Nodes are every node record, oldest first.
|
||||
func (i *Inventory) Nodes(ctx context.Context) ([]Node, error) {
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
|
||||
Reference in New Issue
Block a user