From 91413de335168205dbe5f958aad2e9516cb0a1be Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 26 Oct 2026 19:18:46 +0100 Subject: [PATCH] =?UTF-8?q?compose=20names=20the=20pair=20credentials=20a?= =?UTF-8?q?=20node's=20composition=20reads=20as=20consumer=20and=20provide?= =?UTF-8?q?r,=20and=20frombus=20asks=20them=20sealed=20before=20it=20compo?= =?UTF-8?q?ses=20(issue=201126,=20ADR=200550=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/compose/credentials_needed.go | 64 ++++++++++++++++++++++++++++++ go/compose/credentials_test.go | 9 +++++ go/compose/frombus/frombus.go | 19 ++++++++- go/compose/frombus/frombus_test.go | 4 ++ 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 go/compose/credentials_needed.go diff --git a/go/compose/credentials_needed.go b/go/compose/credentials_needed.go new file mode 100644 index 0000000..a3a9bc1 --- /dev/null +++ b/go/compose/credentials_needed.go @@ -0,0 +1,64 @@ +package compose + +import ( + "fmt" + "sort" +) + +// CredentialKey names one pair credential by the ends it joins: the provision, the consumer's node and module, the +// provider's node, and the local name where a module keeps several (Credential's and Issued's key). +type CredentialKey struct { + Provision string `json:"provision"` + Consumer string `json:"consumer"` + ConsumerModule string `json:"consumer_module"` + Provider string `json:"provider"` + Local string `json:"local,omitempty"` +} + +// CredentialsNeeded is every pair credential node's composition reads (novox/hq ADR 0547 §2, ADR 0550 §5, issue 1126): +// as a consumer, each need of its resolution a provider's credential answers (a need answered by a record is a licence's, +// not a pair's); as a provider, each credential it must carry a grant for (rowsFor). A node that composes its own asks the +// vault for exactly these, sealed to its key, and composes with them; one it does not get is stood in for, which the +// node's own apply refuses (CompositionsApplied). +func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []CredentialKey, err error) { + c := newComposer(in) + resolved, err := c.resolution(node) + if err != nil { + return nil, nil, err + } + for _, n := range resolved.Needs { + if n.ByRecord || n.From == "" { + continue + } + asConsumer = append(asConsumer, CredentialKey{Provision: n.Name, Consumer: node, ConsumerModule: n.For, + Provider: n.From, Local: n.Local}) + } + rows, err := c.rowsFor(node) + if err != nil { + return nil, nil, fmt.Errorf("compose: the credentials %s provides: %w", node, err) + } + for _, r := range rows { + asProvider = append(asProvider, CredentialKey{Provision: r.Provision, Consumer: r.Consumer, + ConsumerModule: r.ConsumerModule, Provider: node, Local: r.Local}) + } + sortKeys(asConsumer) + sortKeys(asProvider) + return asConsumer, asProvider, nil +} + +func sortKeys(ks []CredentialKey) { + sort.Slice(ks, func(i, j int) bool { + a, b := ks[i], ks[j] + switch { + case a.Provision != b.Provision: + return a.Provision < b.Provision + case a.Provider != b.Provider: + return a.Provider < b.Provider + case a.Consumer != b.Consumer: + return a.Consumer < b.Consumer + case a.ConsumerModule != b.ConsumerModule: + return a.ConsumerModule < b.ConsumerModule + } + return a.Local < b.Local + }) +} diff --git a/go/compose/credentials_test.go b/go/compose/credentials_test.go index 924df06..0697358 100644 --- a/go/compose/credentials_test.go +++ b/go/compose/credentials_test.go @@ -172,6 +172,15 @@ func aGrantMesh(t *testing.T) Input { // the sealed copy a stand-in is composed and named (novox/hq plan.go grantsFor). func TestAGrantIsCreatedForEachCredentialMadeForAConsumer(t *testing.T) { in := aGrantMesh(t) + // 1126 (ADR 0550 §5): the credentials each node's composition reads, by their ends: laptop's files consumes anchor's + // s3-bucket, and anchor grants it; the node composing its own asks the vault for exactly these. + pair := CredentialKey{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor"} + if consumer, provider, err := CredentialsNeeded(in, "laptop"); err != nil || len(provider) != 0 || len(consumer) != 1 || consumer[0] != pair { + t.Errorf("laptop needs %v as consumer and %v as provider (%v), want %v as consumer", consumer, provider, err, pair) + } + if consumer, provider, err := CredentialsNeeded(in, "anchor"); err != nil || len(consumer) != 0 || len(provider) != 1 || provider[0] != pair { + t.Errorf("anchor needs %v as consumer and %v as provider (%v), want %v as provider", consumer, provider, err, pair) + } got := rendered(t, in, "anchor", Foreseeing) if len(got.Rendering.Grants) != 1 { t.Fatalf("grants are %+v", got.Rendering.Grants) diff --git a/go/compose/frombus/frombus.go b/go/compose/frombus/frombus.go index ae1dba4..1bc86f5 100644 --- a/go/compose/frombus/frombus.go +++ b/go/compose/frombus/frombus.go @@ -19,8 +19,9 @@ // - each module's config values on a node: mesh-config's resolved state, the composing node's slice alone unless the // caller composes every node (ADR 0547 §2, the prepare step's L4). // -// **Own secrets** come from the vault sealed to the composing node's key (OwnSecrets), for that node alone. **Not yet -// read** (each a later subtask of issue 1126): licences, pair credentials, issued credentials and core boxes. Without one +// **Own secrets and pair credentials** come from the vault sealed to the composing node's key (OwnSecrets, Credentials), +// for that node alone: as a consumer and as a provider, exactly the keys compose.CredentialsNeeded names. **Not yet +// read** (each a later subtask of issue 1126): licences (none is placed today) and core boxes. Without one // the composition stands in for it, and Compose refuses a composition that stands in for anything (compose.ErrStoodIn): // a node never applies a stand-in. No node is joining before ADR 0547's third milestone (ADR 0550 §6), so Joining is // always empty. @@ -58,6 +59,10 @@ type Reader interface { // OwnSecrets is the own secrets of the composing node's modules, each sealed to that node's key, as the vault answers // the node's engine (the vault's verb `own`, ADR 0550 §5); asked only for the node composing itself. OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecretRow, error) + // Credentials is, for the composing node, each pair credential it reads as a consumer, held and sealed to its key, + // and each it grants as a provider, with the copy sealed to it (the vault's `own` with the keys, ADR 0550 §5). A key + // the vault holds no value for is left out; the composition then stands in for it, and Compose refuses it. + Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) } // Mesh is what is true of the whole mesh and is no part's state on the bus yet: the private network's range and the @@ -217,6 +222,16 @@ func Compose(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]s if err != nil { return nil, nil, err } + // The pair credentials its composition reads, asked of the vault sealed to it (ADR 0550 §5). + asConsumer, asProvider, err := compose.CredentialsNeeded(in, s.Node) + if err != nil { + return nil, nil, err + } + if len(asConsumer)+len(asProvider) > 0 { + if in.Credentials, in.Issued, err = r.Credentials(ctx, s.Node, asConsumer, asProvider); err != nil { + return nil, nil, fmt.Errorf("frombus: the credentials of %s: %w", s.Node, err) + } + } return compose.CompositionsApplied(in, s.Node, fps) } diff --git a/go/compose/frombus/frombus_test.go b/go/compose/frombus/frombus_test.go index 5eeaafa..c6c5642 100644 --- a/go/compose/frombus/frombus_test.go +++ b/go/compose/frombus/frombus_test.go @@ -43,6 +43,10 @@ func (b *bus) Manifest(_ context.Context, digest string) ([]byte, error) { } func (b *bus) OwnSecrets(context.Context, string) ([]compose.OwnSecretRow, error) { return b.own, nil } +func (b *bus) Credentials(context.Context, string, []compose.CredentialKey, []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) { + return nil, nil, nil +} + func (b *bus) Values(_ context.Context, node, module string) (map[string]any, error) { b.valuesRead = append(b.valuesRead, node+"."+module) return b.values[node+"."+module], nil