A provider with one credential shares it with every consumer, remade for all at once (hq ADR 0158)
An offer may say `"credential": {"own": "<secret>"}`: the provider's own secret is the credential
every consumer of that provision receives, in the shape of a pair credential. The vault keeps one
value, sealed to the provider, to every consumer that holds the provision and to the operator, all
under one generation stamp; a consumer binding later, or `secret rotate` on the provider's secret,
makes a fresh value and seals it to every holder in one act, and the rotate command sends every
holding machine together. An accepted value is sealed to the consumers of the moment and never
remade: a consumer binding after it is refused with the way out (ADR 0113). The named own secret
must say how it is taken (issue 180), so the provider's start applies the file.
A need carries the shared secret's name from either side of the machine boundary; the plan mints a
consumer's copy from the provider's value. Registered manifests keep their bytes.
This commit is contained in:
@@ -127,6 +127,38 @@ type Offer struct {
|
||||
Name string `json:"name"`
|
||||
// Scope defaults to the node, which is where most things must be to be usable.
|
||||
Scope string `json:"scope,omitempty"`
|
||||
// Credential, when set, says this provision's credential is one of the provider's own secrets,
|
||||
// shared by every consumer (novox/hq ADR 0158): software that holds one password or one key
|
||||
// cannot give each consumer a login of its own. The named secret must say how it is taken.
|
||||
Credential *OfferCredential `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
// OfferCredential names which of the provider's own secrets a provision's consumers receive.
|
||||
type OfferCredential struct {
|
||||
Own string `json:"own"`
|
||||
}
|
||||
|
||||
// SharedCredentialOf is the own secret an offer of this module names as the provision's credential,
|
||||
// and whether it names one.
|
||||
func (m Manifest) SharedCredentialOf(provision string) (string, bool) {
|
||||
for _, o := range m.Provides {
|
||||
if o.Name == provision && o.Credential != nil && o.Credential.Own != "" {
|
||||
return o.Credential.Own, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ProvisionsSharing is every provision of this module whose credential is the named own secret.
|
||||
func (m Manifest) ProvisionsSharing(own string) []string {
|
||||
var out []string
|
||||
for _, o := range m.Provides {
|
||||
if o.Credential != nil && o.Credential.Own == own {
|
||||
out = append(out, o.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// At is this offer's scope, with the default applied.
|
||||
@@ -145,26 +177,30 @@ func (o *Offer) UnmarshalJSON(raw []byte) error {
|
||||
return nil
|
||||
}
|
||||
var full struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Credential *OfferCredential `json:"credential,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &full); err != nil {
|
||||
return fmt.Errorf("a provided name is either a string or {name, scope}: %w", err)
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&full); err != nil {
|
||||
return fmt.Errorf("a provided name is either a string or {name, scope, credential}: %w", err)
|
||||
}
|
||||
o.Name, o.Scope = full.Name, full.Scope
|
||||
o.Name, o.Scope, o.Credential = full.Name, full.Scope, full.Credential
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON writes back the short form when there is nothing else to say, so a manifest that
|
||||
// went through the mesh comes out looking like the one that went in.
|
||||
func (o Offer) MarshalJSON() ([]byte, error) {
|
||||
if o.Scope == "" {
|
||||
if o.Scope == "" && o.Credential == nil {
|
||||
return json.Marshal(o.Name)
|
||||
}
|
||||
return json.Marshal(struct {
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope"`
|
||||
}{o.Name, o.Scope})
|
||||
Name string `json:"name"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Credential *OfferCredential `json:"credential,omitempty"`
|
||||
}{o.Name, o.Scope, o.Credential})
|
||||
}
|
||||
|
||||
// Manifest is everything a module says about itself.
|
||||
@@ -1142,6 +1178,23 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
if !name.MatchString(p) {
|
||||
problems = append(problems, fmt.Sprintf("%q is not a usable name to provide", p))
|
||||
}
|
||||
if offer.Credential != nil {
|
||||
own, declared := m.OwnSecrets[offer.Credential.Own]
|
||||
switch {
|
||||
case offer.Credential.Own == "":
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s provides %q with a credential that names no own secret", m.Module, p))
|
||||
case !declared:
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s provides %q with its own secret %q as the credential, and declares no such secret",
|
||||
m.Module, p, offer.Credential.Own))
|
||||
case own.Taken == "":
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s provides %q with its own secret %q as the credential every consumer receives, so "+
|
||||
"the secret must say how the module takes it: \"taken\": \"at-start\" or \"applied\" (ADR 0158)",
|
||||
m.Module, p, offer.Credential.Own))
|
||||
}
|
||||
}
|
||||
if instead, generic := engineGeneric[p]; generic {
|
||||
// A consumer is written against an engine, not a role (novox/hq ADR 0027). Providing
|
||||
// the role means a requirement for it matches any engine, resolves as satisfied, and
|
||||
|
||||
@@ -179,6 +179,10 @@ type Needed struct {
|
||||
// for one requirement (ADR 0094); empty for the ordinary one. Part of what identifies the pair
|
||||
// credential, so two secrets from one provider to one module are two secrets.
|
||||
Local string
|
||||
// SharedOwn is set when the provision's credential is one of the provider's own secrets, shared
|
||||
// by every consumer (novox/hq ADR 0158): the name of that secret in the provider's definition.
|
||||
// The plan mints the pair's copy from the provider's value rather than a value of its own.
|
||||
SharedOwn string
|
||||
// Manager is set when this holder is a refreshable-grant licence's MANAGER, delivered the refresh
|
||||
// token rather than an access token (novox/hq ADR 0050). It changes one thing downstream: an empty
|
||||
// Sealed is tolerated — the manager has not adopted a refresh token yet, which is a real waiting
|
||||
@@ -322,7 +326,8 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
||||
}
|
||||
needs = append(needs, Needed{
|
||||
Name: want, From: node.Name, At: at,
|
||||
Serves: servedHere(catalogue, chosen, want), For: because[want]})
|
||||
Serves: servedHere(catalogue, chosen, want), For: because[want],
|
||||
SharedOwn: sharedHere(catalogue, chosen, want)})
|
||||
} else if served := servedHere(catalogue, chosen, want); len(served) > 0 {
|
||||
// Answered here with no credential to mint, but the provider serves facts the
|
||||
// consumer cannot guess — a port, a model name — and so still needs a binding.
|
||||
@@ -369,8 +374,12 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
||||
node.Name, want, p.Node, meshNetwork))
|
||||
return
|
||||
}
|
||||
shared := ""
|
||||
if pm, known := catalogue[p.Module]; known {
|
||||
shared, _ = pm.SharedCredentialOf(want)
|
||||
}
|
||||
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At,
|
||||
Serves: p.Serves, For: because[want]})
|
||||
Serves: p.Serves, For: because[want], SharedOwn: shared})
|
||||
}
|
||||
switch {
|
||||
case world.Unchecked:
|
||||
@@ -588,6 +597,20 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
||||
// need that is never created is a binding the consumer never gets. It is right about that from the
|
||||
// manifest alone, which is why walking the catalogue mid-resolution is enough here and is not
|
||||
// enough for the values.
|
||||
// sharedHere is the own secret the provider of a provision on this same machine names as its
|
||||
// credential (ADR 0158), or "" when the provider gives each consumer its own.
|
||||
func sharedHere(catalogue map[string]Manifest, chosen map[string]bool, want string) string {
|
||||
for name, m := range catalogue {
|
||||
if !chosen[name] {
|
||||
continue
|
||||
}
|
||||
if own, shared := m.SharedCredentialOf(want); shared {
|
||||
return own
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func servedHere(catalogue map[string]Manifest, chosen map[string]bool, want string) map[string]any {
|
||||
for name, m := range catalogue {
|
||||
if !chosen[name] {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A provider with one credential shares it (novox/hq ADR 0158): the offer names the own secret, the
|
||||
// secret says how it is taken, and a consumer's need carries the name so the plan mints its copy
|
||||
// from the provider's value.
|
||||
func TestAnOfferMayNameAnOwnSecretAsItsCredential(t *testing.T) {
|
||||
m, err := ParseManifest([]byte(`{"module":"downloader","version":"1",
|
||||
"own-secrets":{"password":{"path":"/var/lib/mesh/downloader/password","taken":"at-start"}},
|
||||
"provides":[{"name":"downloader-api","credential":{"own":"password"}}],
|
||||
"serves":{"downloader-api":{"port":8080,"username":"admin"}}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if own, shared := m.SharedCredentialOf("downloader-api"); !shared || own != "password" {
|
||||
t.Fatalf("the offer's credential was not read: %v %v", own, shared)
|
||||
}
|
||||
if got := m.ProvisionsSharing("password"); len(got) != 1 || got[0] != "downloader-api" {
|
||||
t.Fatalf("the provisions sharing the secret: %v", got)
|
||||
}
|
||||
|
||||
for want, raw := range map[string]string{
|
||||
"declares no such secret": `{"module":"d","version":"1","provides":[{"name":"d-api","credential":{"own":"password"}}]}`,
|
||||
"must say how the module takes it": `{"module":"d","version":"1","own-secrets":{"password":"/p"},
|
||||
"provides":[{"name":"d-api","credential":{"own":"password"}}]}`,
|
||||
"names no own secret": `{"module":"d","version":"1","provides":[{"name":"d-api","credential":{"own":""}}]}`,
|
||||
} {
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("expected a refusal saying %q, got %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sharingShelf() map[string]Manifest {
|
||||
return shelf(
|
||||
Manifest{Module: "downloader", Version: "1",
|
||||
Provides: []Offer{{Name: "downloader-api", Scope: ScopeMesh, Credential: &OfferCredential{Own: "password"}}},
|
||||
OwnSecrets: OwnSecrets{"password": {Path: "/var/lib/mesh/downloader/password", Taken: TakenAtStart}},
|
||||
Serves: map[string]map[string]any{"downloader-api": {"port": 8080, "username": "admin"}}},
|
||||
Manifest{Module: "manager", Version: "1", Requires: []string{"downloader-api"}},
|
||||
)
|
||||
}
|
||||
|
||||
func TestAConsumersNeedCarriesTheSharedSecretsName(t *testing.T) {
|
||||
// On the same machine.
|
||||
together, err := Resolve(sharingShelf(), []string{"downloader", "manager"}, workstation(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, n := range together.Needs {
|
||||
if n.Name == "downloader-api" && n.For == "manager" {
|
||||
found = true
|
||||
if n.SharedOwn != "password" {
|
||||
t.Fatalf("the need on one machine does not name the shared secret: %+v", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("the manager's need was not resolved: %+v", together.Needs)
|
||||
}
|
||||
// Across machines, the provider known by its module.
|
||||
apart, err := Resolve(sharingShelf(), []string{"manager"}, onBoth("example.tld"), World{
|
||||
Offered: map[string][]Provider{"downloader-api": {{Node: "home-server", At: "home-server.internal",
|
||||
Module: "downloader", Serves: map[string]any{"port": 8080}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, n := range apart.Needs {
|
||||
if n.Name == "downloader-api" && n.SharedOwn != "password" {
|
||||
t.Fatalf("the need across machines does not name the shared secret: %+v", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user