package inventory import ( "context" "errors" "fmt" "sort" "github.com/jackc/pgx/v5" "github.com/novox/mesh-controller/internal/secrets" ) // The reads of what a composition would make or hold, in one place and writing nothing (design 47 R4b, issue 1383): the // controller's adapter from its stores to mesh-sdk's composition input asks them once for the whole mesh, where the // composition's own reads (SecretFor, SharedSecretFor, ModuleSecretIfIssued, GivenOwnSecret) ask for one pair or one // secret and make what is not held. // OwnSecretRow is one own secret held on a machine (a module_secret row): who made it (`made`, or `accepted` for a // value a person gave), the sealing key it is sealed to, and the value sealed to that key. type OwnSecretRow struct { Node, Module, Name, Origin, SealedTo, Sealed string } // OwnSecretRows is every own secret held on every machine, by machine, module and name. It writes nothing. func (i *Inventory) OwnSecretRows(ctx context.Context) ([]OwnSecretRow, error) { rows, err := i.store.Pool().Query(ctx, `select n.name, m.module, m.name, m.origin, m.node_key, m.sealed from module_secret m join node n on n.id = m.node order by n.name, m.module, m.name`) if err != nil { return nil, err } defer rows.Close() var out []OwnSecretRow for rows.Next() { var r OwnSecretRow if err := rows.Scan(&r.Node, &r.Module, &r.Name, &r.Origin, &r.SealedTo, &r.Sealed); err != nil { return nil, err } out = append(out, r) } return out, rows.Err() } // PairState is how one pair credential stands, as the composition's reads of it would find it (what SecretFor, // SharedSecretFor and GivenPairSecret answer), without making anything. type PairState struct { // State is one of the Pair constants. State string // Sealed is the consumer's copy as it is sealed to the consumer's key, for a held credential. Sealed string // Why is the controller's words for a credential not given or refused. Why string } // The states of a pair credential. const ( // PairHeld is a credential held, sealed to the keys both ends have now. PairHeld = "held" // PairWouldMake is a credential the next send makes. PairWouldMake = "would-make" // PairNotGiven is a credential only a person gives (novox/hq ADR 0337), which nobody gave under the keys the ends have now. PairNotGiven = "not-given" // PairRefused is a credential the next send would be refused when it made it, for the reason Why gives. PairRefused = "refused" ) // PairCredentialKind says which of the controller's reads a pair is answered by. type PairCredentialKind int const ( // PairPlain is a credential made for the pair (SecretFor). PairPlain PairCredentialKind = iota // PairShared is the provider's one credential sealed to the consumer too (SharedSecretFor; Own names the provider's own secret). PairShared // PairOutside is a credential only a person gives (GivenPairSecret). PairOutside ) // PairCredential is one pair credential, by the ends it joins. type PairCredential struct { Kind PairCredentialKind Name, Consumer, ConsumerModule, Provider, ProviderModule, Local string // Own is, for a shared credential, the provider's own secret it is. Own string } // PairCredentialState says how a pair credential stands now, by the rules the next send applies and without applying // them: nothing is inserted, remade or sealed. A credential the send would make is PairWouldMake, unless the send would be // refused (a machine with no sealing key, a value given and sealed to a key one end no longer has), which is PairRefused // in the send's own words. func (i *Inventory) PairCredentialState(ctx context.Context, p PairCredential) (PairState, error) { switch p.Kind { case PairOutside: return i.outsideState(ctx, p) case PairShared: return i.sharedState(ctx, p) } return i.plainState(ctx, p) } func (i *Inventory) outsideState(ctx context.Context, p PairCredential) (PairState, error) { got, err := i.GivenPairSecret(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.ProviderModule, p.Local) var notGiven *NotGivenError switch { case errors.As(err, ¬Given): return PairState{State: PairNotGiven, Why: notGiven.Why}, nil case err != nil: return PairState{}, err } return PairState{State: PairHeld, Sealed: got.ForConsumer}, nil } func (i *Inventory) plainState(ctx context.Context, p PairCredential) (PairState, error) { consumerKey, err := i.SealingKeyOf(ctx, p.Consumer) if err != nil { return PairState{}, err } providerKey, err := i.SealingKeyOf(ctx, p.Provider) if err != nil { return PairState{}, err } consumerNode, err := i.NodeByName(ctx, p.Consumer) if err != nil { return PairState{}, err } providerNode, err := i.NodeByName(ctx, p.Provider) if err != nil { return PairState{}, err } var held Secret err = i.store.Pool().QueryRow(ctx, `select for_consumer, for_provider, consumer_key, provider_key, origin from secret where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`, p.Name, consumerNode.ID, p.ConsumerModule, providerNode.ID, p.Local). Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return PairState{}, err } if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey { return PairState{State: PairHeld, Sealed: held.ForConsumer}, nil } if err == nil && held.Origin == OriginAccepted { return PairState{State: PairRefused, Why: fmt.Sprintf( "%s's %q credential from %s was accepted from a person, and a sealing key at one end "+ "has changed since. The mesh cannot re-seal a value it does not hold: accept it "+ "again with `secret accept %s %s %s --provider %s%s`", p.ConsumerModule, p.Name, p.Provider, p.Consumer, p.ConsumerModule, p.Name, p.Provider, localFlag(p.Local))}, nil } // What making one would meet: the operator's key and the two sealing keys, by the same call that seals them. operator, err := i.OperatorKey(ctx) if err != nil { return PairState{}, err } if _, _, merr := secrets.MakeWithOperator(consumerKey, providerKey, operator); merr != nil { return PairState{State: PairRefused, Why: merr.Error()}, nil } return PairState{State: PairWouldMake}, nil } func (i *Inventory) sharedState(ctx context.Context, p PairCredential) (PairState, error) { consumerKey, err := i.SealingKeyOf(ctx, p.Consumer) if err != nil { return PairState{}, err } consumerNode, err := i.NodeByName(ctx, p.Consumer) if err != nil { return PairState{}, err } providerNode, err := i.NodeByName(ctx, p.Provider) if err != nil { return PairState{}, err } providerKey, err := i.SealingKeyOf(ctx, p.Provider) if err != nil { return PairState{}, err } if consumerKey == "" || providerKey == "" { return PairState{State: PairRefused, Why: fmt.Sprintf("%s and %s both need a sealing key before %s can be shared", p.Consumer, p.Provider, p.Name)}, nil } var ownGeneration, ownOrigin, ownKey *string err = i.store.Pool().QueryRow(ctx, `select generation, origin, node_key from module_secret where node = $1 and module = $2 and name = $3`, providerNode.ID, p.ProviderModule, p.Own).Scan(&ownGeneration, &ownOrigin, &ownKey) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return PairState{}, err } var held Secret var pairGeneration *string err = i.store.Pool().QueryRow(ctx, `select for_consumer, for_provider, consumer_key, provider_key, origin, generation from secret where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`, p.Name, consumerNode.ID, p.ConsumerModule, providerNode.ID, p.Local). Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin, &pairGeneration) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return PairState{}, err } current := ownGeneration != nil && pairGeneration != nil && *ownGeneration == *pairGeneration && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey && ownKey != nil && *ownKey == providerKey if current { return PairState{State: PairHeld, Sealed: held.ForConsumer}, nil } if _, outside, err := i.ownIssuedOutside(ctx, p.ProviderModule, p.Own); err != nil { return PairState{}, err } else if outside && (ownOrigin == nil || *ownOrigin != OriginAccepted) { return PairState{State: PairNotGiven, Why: "nobody gave " + p.ProviderModule + "'s own secret " + p.Own + ", which is issued outside the mesh"}, nil } if ownOrigin != nil && *ownOrigin == OriginAccepted { return PairState{State: PairRefused, Why: fmt.Sprintf( "%s on %s needs %s from %s, whose credential is %s's own secret %q — a value given to the "+ "mesh, which cannot seal it to a consumer that binds later (ADR 0158): `secret accept %s %s %s` "+ "again, which seals it to every current consumer", p.ConsumerModule, p.Consumer, p.Name, p.Provider, p.ProviderModule, p.Own, p.Provider, p.ProviderModule, p.Own)}, nil } return PairState{State: PairWouldMake}, nil } // BusMinted is the name of every bus user the mesh has minted a password hash for: `.` for a module and // `provisioner.` for a machine's provisioner client. func (i *Inventory) BusMinted(ctx context.Context) ([]string, error) { kept, err := i.BusUsers(ctx) if err != nil { return nil, err } out := make([]string, 0, len(kept)) for name := range kept { out = append(out, name) } sort.Strings(out) return out, nil }