213 lines
7.8 KiB
Go
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)
|
|
}
|