Files
mesh-delivery/modules/mesh-grants/internal/grants/accounts.go
T

213 lines
7.8 KiB
Go

package grants
import (
"encoding/json"
"fmt"
"slices"
"sort"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// **The accounts the mesh wants, said to the vault** (novox/hq ADR 0561 §2, issue 774). The bus composer derives every
// user from placement, the manifests and the wants; for each account the vault mints (a module's, a process's, a
// machine's provisioner client's), the part writes one key of its state `accounts`, keyed by the bus user, with the
// fields the controller's credential carries beside the password. A key is written only when it differs, and deleted
// only when placement no longer names its user and every input of the pass was read: a refused read never retires an
// account. The vault alone reads the state (buscompose.NamedReaders); it mints each user it holds no value for.
// StateAccounts is the part's state of wanted accounts.
const StateAccounts = buscompose.AccountsState
// AccountsReaders are the only modules that may read StateAccounts.
var AccountsReaders = buscompose.NamedReaders[buscompose.GrantsAccounts]
// Announced is what the part says it keeps: every state readable by any reader, but the wanted accounts, by the vault
// alone (ADR 0495 §2-§3, ADR 0561 §2).
func Announced() []announce.State {
var out []announce.State
for _, s := range States {
if s == StateAccounts {
out = append(out, announce.State{Name: s, Readers: slices.Clone(AccountsReaders)})
continue
}
out = append(out, announce.State{Name: s, Readable: true})
}
return out
}
// BusAddress is where every node on the private network reaches the bus, and its certificate's fingerprint: the part's
// own credential says both, since the controller gave every node's account the bus holder's overlay name (its
// brokerReachableAt).
type BusAddress struct {
URL string
Fingerprint string
}
// WantedAccounts is every account the vault mints that the records compose, keyed by user, with the bus's address. A
// node, the controller, an enrolment user or a person is never one (ADR 0561 §4, §5). Pure.
func WantedAccounts(in Inputs, recs buscompose.Records, bus BusAddress) (map[string]buscompose.WantedAccount, error) {
principals, err := buscompose.Users(recs)
if err != nil {
return nil, err
}
out := map[string]buscompose.WantedAccount{}
for _, p := range principals {
w := buscompose.WantedAccount{User: p.Username(), Kind: p.Kind, Node: p.Node, Module: p.Module, Process: p.Process,
URL: bus.URL, Fingerprint: bus.Fingerprint}
switch p.Kind {
case buscompose.KindModule, buscompose.KindNodeTools:
if m, ok := manifestOn(in, p.Node, p.Module); ok {
w.Claims = ClaimsOf(m)
}
case buscompose.KindProcess, buscompose.KindProvisioner:
default:
continue
}
out[w.User] = w
}
return out, nil
}
// manifestOn is the manifest a node runs of module: the build its want names, else the trunk's.
func manifestOn(in Inputs, node, module string) (catalogue.Manifest, bool) {
if m, ok := in.Running[node+"."+module]; ok {
return m, true
}
m, ok := in.Manifests[module]
return m, ok
}
// ClaimsOf is a module's claims as its credential carries them: the controller's claimsFor, over the mesh's own seats
// (its seat table is seeded from them): each claim's seat and scope, and the verbs the runtime serves for it, the claim's
// own or every verb the seat promises; a seat the mesh does not define keeps the claim's scope and no verbs.
func ClaimsOf(m catalogue.Manifest) []buscompose.ClaimedSeat {
if len(m.Claims) == 0 {
return nil
}
byName := map[string]catalogue.Seat{}
for _, s := range catalogue.DefaultSeats() {
byName[s.Name] = s
}
var out []buscompose.ClaimedSeat
for _, c := range m.Claims {
claimed := buscompose.ClaimedSeat{Seat: c.Name, Scope: c.At()}
if s, known := byName[c.Name]; known {
claimed.Scope = s.Scope
claimed.Serves = c.ServesFor(catalogue.Manifest{Tools: catalogue.VerbNames(s.Serves)})
}
out = append(out, claimed)
}
return out
}
// Accounts answers the tool accounts (ADR 0561 §9): the wanted accounts as the part last wrote them, one user's or all,
// each with when it was written; and what the last pass did to them. Never a password: the state holds none.
func (p *Part) Accounts(user string) (any, error) {
users := []string{user}
if user == "" {
keys, err := p.Store.Keys(StateAccounts)
if err != nil {
return nil, fmt.Errorf("its own %s cannot be listed: %w", StateAccounts, err)
}
users = keys
}
out := []buscompose.WantedAccount{}
for _, u := range users {
raw, _, found, err := p.Store.Get(StateAccounts, u)
if err != nil {
return nil, fmt.Errorf("its own %s %s cannot be read: %w", StateAccounts, u, err)
}
if !found {
if user != "" {
return nil, fmt.Errorf("no account %s is wanted: placement composes no module, process or provisioner client of that user", user)
}
continue
}
w, err := buscompose.ReadWantedAccount(u, raw)
if err != nil {
return nil, err
}
out = append(out, w)
}
answer := map[string]any{"accounts": out, "count": len(out), "read_by": AccountsReaders}
if raw, _, found, err := p.Store.Get(StateUserList, KeyPass); err == nil && found {
var res Result
if json.Unmarshal(raw, &res) == nil && res.Accounts != nil {
answer["last_pass"] = res.Accounts
}
}
return answer, nil
}
// AccountsWritten is what one pass did to the state of wanted accounts.
type AccountsWritten struct {
Wanted int `json:"wanted"`
Written []string `json:"written,omitempty"`
Deleted []string `json:"deleted,omitempty"`
// Kept are keys placement no longer names that this pass did not delete, with why.
Kept []string `json:"kept,omitempty"`
}
// writeAccounts writes each wanted account whose key differs, and deletes the key of a user no longer placed, but only
// when every input of the pass was read.
func (p *Part) writeAccounts(res *Result, in Inputs, recs buscompose.Records) {
if p.Bus.URL == "" {
res.Errors = append(res.Errors, "the wanted accounts are not written: the part knows no bus address to give them (its own credential names none)")
return
}
wanted, err := WantedAccounts(in, recs, p.Bus)
if err != nil {
res.Errors = append(res.Errors, "the wanted accounts could not be composed: "+err.Error())
return
}
done := &AccountsWritten{Wanted: len(wanted)}
res.Accounts = done
for _, user := range sortedKeys(wanted) {
w := wanted[user]
if raw, _, found, err := p.Store.Get(StateAccounts, user); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", StateAccounts, user, err))
continue
} else if found {
if was, err := buscompose.ReadWantedAccount(user, raw); err == nil && was.Same(w) {
continue
}
}
w.At = p.now()
data, _ := json.Marshal(w)
if _, err := p.Store.Put(StateAccounts, user, data); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateAccounts, user, err))
continue
}
done.Written = append(done.Written, user)
}
held, err := p.Store.Keys(StateAccounts)
if err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be listed, so no key of a user gone is deleted: %v", StateAccounts, err))
return
}
failed := in.ReadFailures()
placed := Placed(in)
for _, user := range held {
if _, still := wanted[user]; still {
continue
}
switch {
case placed(user):
done.Kept = append(done.Kept, user+": placement still names it (composed with no account this pass)")
continue
case len(failed) > 0:
done.Kept = append(done.Kept, user+": a read of this pass failed, so no account is retired")
continue
}
if err := p.Store.Delete(StateAccounts, user); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be deleted: %v", StateAccounts, user, err))
continue
}
done.Deleted = append(done.Deleted, user)
}
sort.Strings(done.Kept)
}