Merge pull request 'compose: an offer of form none composes no credential and holds nothing (issue 1126)' (#159) from fix/1126-provision-credentials into main

This commit was merged in pull request #159.
This commit is contained in:
2026-11-02 15:31:34 +00:00
7 changed files with 118 additions and 26 deletions
+5
View File
@@ -846,6 +846,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
// and nothing would say so. // and nothing would say so.
continue continue
} }
if g.Sealed == "" {
// Its offer presents no credential (form none, issue 1126): nothing to write, and an empty credential
// file would be read as one that failed. Who asked still reaches the provider in its contributions.
continue
}
first = append(first, ownedBy(r.provisionsAs(m), map[string]any{ first = append(first, ownedBy(r.provisionsAs(m), map[string]any{
// One file per holder — the consumer's module with its local name after it // One file per holder — the consumer's module with its local name after it
// where it keeps several (ADR 0094); the lab found two files with one id. // where it keeps several (ADR 0094); the lab found two files with one id.
+17
View File
@@ -66,6 +66,23 @@ func TestAGrantSecretIsOwnedByTheAccountThatProvisions(t *testing.T) {
"root-owned 0600 file is one it cannot read — which is silent, because the harness "+ "root-owned 0600 file is one it cannot read — which is silent, because the harness "+
"calls it \"not readable yet\"", fmtPath(secret), got, "operator") "calls it \"not readable yet\"", fmtPath(secret), got, "operator")
} }
// A grant whose offer presents no credential (form none, issue 1126) carries nothing sealed: no secret file is
// written for it, as none is for a need with no credential, and the provider still learns who asked.
out, err = r.Declaration(Rendering{Grants: []Grant{{
Provision: "mongodb-database", Consumer: "workstation", From: "photos", Slug: "photos", Values: map[string]any{}}}})
if err != nil {
t.Fatal(err)
}
asked := false
for _, res := range out {
if res["type"] == "file" && strings.HasSuffix(fmtPath(res), ".secret") {
t.Errorf("a grant with no credential writes %v", res)
}
asked = asked || fmtPath(res) == "/var/lib/mongodb/grants/mesh.json"
}
if !asked {
t.Errorf("the provider no longer learns who asked: %v", out)
}
} }
// And a provider whose code still runs in a container keeps the owner it declares, so this // And a provider whose code still runs in a container keeps the owner it declares, so this
+1 -1
View File
@@ -372,7 +372,7 @@ func TestWhatAModuleOfItsOwnAccountReadsIsItsAccounts(t *testing.T) {
with := Rendering{ArtifactStore: "anchor.internal:5101", with := Rendering{ArtifactStore: "anchor.internal:5101",
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}, "vault": {"broker": "own"}}, Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}, "vault": {"broker": "own"}},
Kept: &KeptExport{Export: 1, OperatorKey: "k"}, Kept: &KeptExport{Export: 1, OperatorKey: "k"},
Grants: []Grant{{Provision: "secret", Consumer: "anchor", From: "telegram", Grants: []Grant{{Provision: "secret", Consumer: "anchor", From: "telegram", Sealed: "sealed-to-anchor",
Values: map[string]any{"name": "telegram"}}}} Values: map[string]any{"name": "telegram"}}}}
goRuntime := Manifest{Module: RuntimeModule, Version: "1", goRuntime := Manifest{Module: RuntimeModule, Version: "1",
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}}, OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}},
+18 -1
View File
@@ -105,7 +105,12 @@ func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overfl
} }
} }
sealed := s.ForProvider sealed := s.ForProvider
if sealed == "" { switch {
case credentiallessRow(plan, s, node):
// Its offer presents no credential (form none): the grant carries the consumer and nothing sealed, and
// nothing stands in for it, so neither end waits for a value nobody makes (issue 1126).
sealed = ""
case sealed == "":
sealed = catalogue.ForeseenSealed sealed = catalogue.ForeseenSealed
foreseen = append(foreseen, fmt.Sprintf("the grant of %s for %s on %s", s.Provision, s.ConsumerModule, s.Consumer)) foreseen = append(foreseen, fmt.Sprintf("the grant of %s for %s on %s", s.Provision, s.ConsumerModule, s.Consumer))
} }
@@ -116,6 +121,18 @@ func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overfl
return out, withheld, unbound, foreseen, nil return out, withheld, unbound, foreseen, nil
} }
// credentialless is whether a need's provider offers it with no credential (catalogue.CredentialFormNone): the vault
// mints nothing for it, so nothing is asked of the vault, sealed into a grant or a file, or stood in for (issue 1126).
func credentialless(n catalogue.Needed) bool { return n.CredentialForm == catalogue.CredentialFormNone }
// credentiallessRow is whether the consumer's resolution answers row's need from provider with an offer of form none.
func credentiallessRow(consumer catalogue.Resolution, row Issued, provider string) bool {
return slices.ContainsFunc(consumer.Needs, func(n catalogue.Needed) bool {
return n.Name == row.Provision && n.For == row.ConsumerModule && n.Local == row.Local && n.From == provider &&
credentialless(n)
})
}
// rowsFor is every pair credential this node's declaration must carry a grant for, in the order the controller reads them // rowsFor is every pair credential this node's declaration must carry a grant for, in the order the controller reads them
// (SecretsFrom: by provision, consumer, consumer module and local name): the ones the Input says were made, and the ones the same send // (SecretsFrom: by provision, consumer, consumer module and local name): the ones the Input says were made, and the ones the same send
// would make before it composes this node -- every need of a node that resolves (this one too), whose provider is this node, that is not // would make before it composes this node -- every need of a node that resolves (this one too), whose provider is this node, that is not
+7 -2
View File
@@ -19,7 +19,8 @@ type CredentialKey struct {
// as a consumer, each need of its resolution a provider's credential answers (a need answered by a record is a licence's, // 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 // 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 // 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). // node's own apply refuses (CompositionsApplied). A need whose provider offers it with no credential (form none) is
// in neither list: the vault mints nothing for it (issue 1126).
func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []CredentialKey, err error) { func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []CredentialKey, err error) {
c := newComposer(in) c := newComposer(in)
resolved, err := c.resolution(node) resolved, err := c.resolution(node)
@@ -27,7 +28,7 @@ func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []Credenti
return nil, nil, err return nil, nil, err
} }
for _, n := range resolved.Needs { for _, n := range resolved.Needs {
if n.ByRecord || n.From == "" { if n.ByRecord || n.From == "" || credentialless(n) {
continue continue
} }
asConsumer = append(asConsumer, CredentialKey{Provision: n.Name, Consumer: node, ConsumerModule: n.For, asConsumer = append(asConsumer, CredentialKey{Provision: n.Name, Consumer: node, ConsumerModule: n.For,
@@ -38,6 +39,10 @@ func CredentialsNeeded(in Input, node string) (asConsumer, asProvider []Credenti
return nil, nil, fmt.Errorf("compose: the credentials %s provides: %w", node, err) return nil, nil, fmt.Errorf("compose: the credentials %s provides: %w", node, err)
} }
for _, r := range rows { for _, r := range rows {
// A grant whose offer presents no credential (form none) has none to ask for (issue 1126).
if consumer, err := c.planFor(r.Consumer); err == nil && credentiallessRow(consumer.Plan, r, node) {
continue
}
asProvider = append(asProvider, CredentialKey{Provision: r.Provision, Consumer: r.Consumer, asProvider = append(asProvider, CredentialKey{Provision: r.Provision, Consumer: r.Consumer,
ConsumerModule: r.ConsumerModule, Provider: node, Local: r.Local}) ConsumerModule: r.ConsumerModule, Provider: node, Local: r.Local})
} }
+66 -22
View File
@@ -384,30 +384,74 @@ func TestNoSecretValueOfTheInputIsInWhatTheCompositionSays(t *testing.T) {
} }
} }
// Round 1, finding 2: a consumer the same send adds has no credential in the Input yet, and the provider's declaration of that send // A consumer no credential was made for yet is granted with a stand-in and named foreseen, for every form whose credential
// carries its grant, composed with a stand-in and named foreseen. // the vault mints or takes in; an offer of form none has no credential (issue 1126, 2026-11-02: mesh-cli on ace held for
func TestAConsumerNoCredentialWasMadeForYetIsGrantedWithAStandInAndNamedForeseen(t *testing.T) { // "the grant of mcp-endpoint", whose provider node-tools offers form none): its grant carries the consumer and no
in := aGrantMesh(t) // credential, nothing stands in on either side, and neither end asks the vault for one.
in.Issued = nil func TestAConsumerNoCredentialWasMadeForYetIsGrantedWithAStandInUnlessItsOfferHasNone(t *testing.T) {
got := rendered(t, in, "anchor", Foreseeing) for _, tc := range []struct {
if len(got.Rendering.Grants) != 1 { form string
t.Fatalf("grants are %+v", got.Rendering.Grants) cred *catalogue.OfferCredential
} }{
g := got.Rendering.Grants[0] {"undeclared", nil},
if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.Sealed != catalogue.ForeseenSealed { {catalogue.CredentialFormPair, &catalogue.OfferCredential{Form: catalogue.CredentialFormPair}},
t.Errorf("the grant is %+v", g) {catalogue.CredentialFormShared, &catalogue.OfferCredential{Own: "access-key"}},
} {catalogue.CredentialFormNone, &catalogue.OfferCredential{Form: catalogue.CredentialFormNone}},
if !has(got.Foreseen, "the grant of s3-bucket for files on laptop") { } {
t.Errorf("foreseen %v", got.Foreseen) in := aGrantMesh(t)
} in.Issued = nil
// Only the provider's own: the consumer is granted nothing, and nothing in the Input changed. provider := in.Catalogue["objects"]
if len(rendered(t, in, "laptop", Foreseeing).Rendering.Grants) != 0 { provider.Provides[0].Credential = tc.cred
t.Error("the consumer is granted its own credential") if tc.cred != nil && tc.cred.Own != "" {
} provider.OwnSecrets = catalogue.OwnSecrets{"access-key": {Path: "/var/lib/mesh/objects/access-key"}}
if len(in.Issued) != 0 { }
t.Error("a row was added to the Input") in.Catalogue["objects"] = provider
none := tc.form == catalogue.CredentialFormNone
got := rendered(t, in, "anchor", Foreseeing)
if len(got.Rendering.Grants) != 1 {
t.Fatalf("%s: grants are %+v", tc.form, got.Rendering.Grants)
}
g, wantSealed := got.Rendering.Grants[0], catalogue.ForeseenSealed
if none {
wantSealed = ""
}
if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.Sealed != wantSealed {
t.Errorf("%s: the grant is %+v", tc.form, g)
}
if has(got.Foreseen, "the grant of s3-bucket for files on laptop") == none {
t.Errorf("%s: foreseen %v", tc.form, got.Foreseen)
}
consumer, asProvider, err := CredentialsNeeded(in, "laptop")
_, anchorProvides, err2 := CredentialsNeeded(in, "anchor")
if err != nil || err2 != nil || len(asProvider) != 0 || (len(consumer) == 0) != none || (len(anchorProvides) == 0) != none {
t.Errorf("%s: laptop asks %v, anchor provides %v (%v %v)", tc.form, consumer, anchorProvides, err, err2)
}
resolved, err := Resolve(in, "laptop")
if err != nil {
t.Fatalf("%s: %v", tc.form, err)
}
if n := theFilesNeed(t, resolved); (n.Sealed == "") != none || (len(resolved.Foreseen) == 0) != none {
t.Errorf("%s: the consumer's credential is %q, foreseen %v", tc.form, n.Sealed, resolved.Foreseen)
}
if lap := rendered(t, in, "laptop", Foreseeing); none && len(lap.Foreseen) != 0 {
t.Errorf("%s: the consumer stands in for %v", tc.form, lap.Foreseen)
}
// Only the provider's own: the consumer is granted nothing, and nothing in the Input changed.
if len(rendered(t, in, "laptop", Foreseeing).Rendering.Grants) != 0 {
t.Errorf("%s: the consumer is granted its own credential", tc.form)
}
if len(in.Issued) != 0 {
t.Errorf("%s: a row was added to the Input", tc.form)
}
// A row made long ago for an offer of form none (the controller's) carries no credential either.
in.Issued = []Issued{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", ForProvider: "sealed-to-anchor"}}
if g := rendered(t, in, "anchor", Foreseeing).Rendering.Grants; none && (len(g) != 1 || g[0].Sealed != "") {
t.Errorf("%s: an old row's grant is %+v", tc.form, g)
}
} }
// A credential only a person gives and nobody has is no grant. // A credential only a person gives and nobody has is no grant.
in := aGrantMesh(t)
in.Issued = nil
in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialNotGiven}} in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialNotGiven}}
if got := rendered(t, in, "anchor", Foreseeing); len(got.Rendering.Grants) != 0 { if got := rendered(t, in, "anchor", Foreseeing); len(got.Rendering.Grants) != 0 {
t.Errorf("a credential nobody gave is granted: %+v", got.Rendering.Grants) t.Errorf("a credential nobody gave is granted: %+v", got.Rendering.Grants)
+4
View File
@@ -118,6 +118,10 @@ func (c *composer) planForNow(nodeName string) (Resolved, error) {
} }
continue continue
} }
if credentialless(n) {
// Its provider's offer presents no credential (form none): nothing is sealed, made or stood in for (issue 1126).
continue
}
cred, held := c.credential(n, nodeName) cred, held := c.credential(n, nodeName)
_, outside := issuedOutsideBy(shelf, n) _, outside := issuedOutsideBy(shelf, n)
switch { switch {