Merge pull request 'Say a part waiting for the operator as needs-operator and pass its gate; add secret families (issue 386, ADR 0283)' (#209) from feat/386-a-wait-for-the-operator into main
This commit was merged in pull request #209.
This commit is contained in:
@@ -155,7 +155,7 @@ func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared s
|
||||
}
|
||||
// The definition is asked again now, not only when the value was given: one that has since
|
||||
// said the value is an outside party's, or applied, keeps it as given, and the mark stays gone.
|
||||
if own, ok := m.OwnSecrets[d.name]; !ok || !own.MeshMayMake() {
|
||||
if own, _, ok := m.OwnSecrets.Lookup(d.name); !ok || !own.MeshMayMake() {
|
||||
continue
|
||||
}
|
||||
if err := i.remakeOwn(ctx, d.nodeID, key, m, d.module, d.name); err != nil {
|
||||
|
||||
@@ -34,6 +34,17 @@ type ResourceHealth struct {
|
||||
// Root is "never" on an account verdict that judged whether the account can become root without a
|
||||
// person (novox/hq ADR 0266).
|
||||
Root string `json:"root,omitempty"`
|
||||
// Waits is what a waiting resource waits for the operator to give (novox/hq ADR 0283).
|
||||
Waits []Wait `json:"waits,omitempty"`
|
||||
}
|
||||
|
||||
// Wait is one thing a waiting resource waits for the operator to give: exactly one of Secret and Setting
|
||||
// (novox/hq ADR 0283), as link.Wait carries it.
|
||||
type Wait struct {
|
||||
Part string `json:"part"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Setting string `json:"setting,omitempty"`
|
||||
What string `json:"what"`
|
||||
}
|
||||
|
||||
// NodeHealth is a machine's newest statement, as kept.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// A member of a secret family is given by its full name, at the desk too, and a name outside the family is refused
|
||||
// (novox/hq ADR 0283).
|
||||
func TestAMemberIsGivableAtTheDeskAndANameOutsideTheFamilyIsNot(t *testing.T) {
|
||||
m := catalogue.Manifest{Module: "mounts", OwnSecrets: catalogue.OwnSecrets{
|
||||
"smb-password-*": {Path: "/s/smb-password-*.secret", IssuedBy: catalogue.IssuedOutside},
|
||||
}}
|
||||
if err := GivableAtDesk(m, "smb-password-games"); err != nil {
|
||||
t.Fatalf("a member was refused: %v", err)
|
||||
}
|
||||
for _, name := range []string{"smb-password-*", "smb-password-", "smb-password-a/b", "smb-password-A", "smb-credentials"} {
|
||||
err := GivableAtDesk(m, name)
|
||||
if err == nil {
|
||||
t.Errorf("%q was givable", name)
|
||||
} else if !strings.Contains(err.Error(), "smb-password-<name>") {
|
||||
t.Errorf("%q: the refusal does not say the family's form: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A member given is read back as given, sealed to the machine's key; the mesh makes none, so nothing else is.
|
||||
func TestAMemberGivenIsReadAsGivenAndNothingElseIs(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
node, err := inv.AddNode(ctx, "workstation")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key, _ := aSealingKey(t)
|
||||
if err := inv.RecordSealingKey(ctx, node.ID, key); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := catalogue.Manifest{Module: "mounts", Version: "1", OwnSecrets: catalogue.OwnSecrets{
|
||||
"smb-password-*": {Path: "/s/smb-password-*.secret", IssuedBy: catalogue.IssuedOutside},
|
||||
"token": {Path: "/s/token", IssuedBy: catalogue.IssuedOutside},
|
||||
}}
|
||||
if err := inv.RegisterModule(ctx, m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.AcceptSecretForModule(ctx, "workstation", "mounts", "smb-password-games", "hunter2"); err != nil {
|
||||
t.Fatalf("a member was refused: %v", err)
|
||||
}
|
||||
if err := inv.AcceptSecretForModule(ctx, "workstation", "mounts", "smb-pass", "x"); err == nil {
|
||||
t.Fatal("a name of no family was accepted")
|
||||
}
|
||||
members, err := inv.GivenMembers(ctx, "workstation", "mounts", "smb-password-*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(members) != 1 || members[0].Name != "smb-password-games" || !members[0].Current || members[0].Sealed == "" ||
|
||||
strings.Contains(members[0].Sealed, "hunter2") {
|
||||
t.Fatalf("members: %+v", members)
|
||||
}
|
||||
given, err := inv.GivenOwnSecrets(ctx, "workstation", "mounts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, was := given["smb-password-games"]; !was || len(given) != 1 {
|
||||
t.Fatalf("given: %v", given)
|
||||
}
|
||||
// A value the mesh made is not one a person gave.
|
||||
if _, err := inv.SecretForModule(ctx, "workstation", "mounts", "token"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if given, _ := inv.GivenOwnSecrets(ctx, "workstation", "mounts"); len(given) != 1 {
|
||||
t.Fatalf("a value the mesh made counted as given: %v", given)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
@@ -446,7 +447,7 @@ func (i *Inventory) acceptOwn(ctx context.Context, node, module, name, value str
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
own, declared := m.OwnSecrets[name]
|
||||
own, _, declared := m.OwnSecrets.Lookup(name)
|
||||
if !declared {
|
||||
return false, fmt.Errorf("%s does not declare %q as an own secret; %s — a secret it requires from a provider is accepted with `--provider <node> [--local <name>]`, the value the running service already uses (novox/hq ADR 0163)", module, name, declaresOwn(m))
|
||||
}
|
||||
@@ -583,7 +584,7 @@ const BrokerSecret = "broker"
|
||||
// in place of a value given (catalogue.OwnSecret.MeshMayMake). The desk takes only what a person holds and
|
||||
// the mesh cannot make — a bot's token — so nobody is asked to type the mesh's own credential into a prompt.
|
||||
func GivableAtDesk(m catalogue.Manifest, name string) error {
|
||||
own, ok := m.OwnSecrets[name]
|
||||
own, _, ok := m.OwnSecrets.Lookup(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s", m.Module, name, declaresOwn(m))
|
||||
}
|
||||
@@ -602,7 +603,79 @@ func declaresOwn(m catalogue.Manifest) string {
|
||||
if len(m.OwnSecrets) == 0 {
|
||||
return "it declares no own secrets"
|
||||
}
|
||||
return "it declares: " + strings.Join(sortedNames(m.OwnSecrets.Paths()), ", ")
|
||||
names := sortedNames(m.OwnSecrets.Plain().Paths())
|
||||
// A family is said as its members are given (novox/hq ADR 0283): one per part, by its full name.
|
||||
for _, f := range m.OwnSecrets.Families() {
|
||||
names = append(names, catalogue.FamilyPrefix(f)+"<name> (one per part, issued outside the mesh)")
|
||||
}
|
||||
return "it declares: " + strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
// GivenMember is one member of a secret family given on a machine (novox/hq ADR 0283): its full name, its value
|
||||
// sealed to the machine, and whether it is sealed to the key the machine holds now.
|
||||
type GivenMember struct {
|
||||
Name string
|
||||
Sealed string
|
||||
Current bool
|
||||
}
|
||||
|
||||
// GivenMembers is every member of a module's secret family given on a machine, by name. The mesh never makes a
|
||||
// member, so only a value a person gave is one; a machine with no sealing key holds none.
|
||||
func (i *Inventory) GivenMembers(ctx context.Context, node, module, family string) ([]GivenMember, error) {
|
||||
key, err := i.SealingKeyOf(ctx, node)
|
||||
if err != nil || key == "" {
|
||||
return nil, err
|
||||
}
|
||||
record, err := i.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefix := catalogue.FamilyPrefix(family)
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select name, sealed, node_key from module_secret
|
||||
where node = $1 and module = $2 and origin = 'accepted' and left(name, length($3)) = $3
|
||||
order by name`, record.ID, module, prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []GivenMember
|
||||
for rows.Next() {
|
||||
var g GivenMember
|
||||
var against string
|
||||
if err := rows.Scan(&g.Name, &g.Sealed, &against); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Current = against == key
|
||||
out = append(out, g)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GivenOwnSecrets is every own secret of a module a person gave on a machine, by name, with when (novox/hq ADR
|
||||
// 0283): what the controller checks a module's wait for a secret against. A value the mesh made is not one.
|
||||
func (i *Inventory) GivenOwnSecrets(ctx context.Context, node, module string) (map[string]time.Time, error) {
|
||||
record, err := i.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := i.store.Pool().Query(ctx,
|
||||
`select name, coalesce(made_at, now()) from module_secret where node = $1 and module = $2 and origin = 'accepted'`,
|
||||
record.ID, module)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]time.Time{}
|
||||
for rows.Next() {
|
||||
var name string
|
||||
var at time.Time
|
||||
if err := rows.Scan(&name, &at); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[name] = at
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func sortedNames(of map[string]string) []string {
|
||||
@@ -645,7 +718,7 @@ func (i *Inventory) RotateModuleSecret(ctx context.Context, node, module, name s
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
own, declared := m.OwnSecrets[name]
|
||||
own, _, declared := m.OwnSecrets.Lookup(name)
|
||||
if !declared {
|
||||
return fmt.Errorf("%s does not declare %q as an own secret; %s — a secret it requires from a provider is accepted with `--provider <node> [--local <name>]`, the value the running service already uses (novox/hq ADR 0163)", module, name, declaresOwn(m))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user