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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"}},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user