diff --git a/go/compose/credentials.go b/go/compose/credentials.go index e6aef49..3114c8a 100644 --- a/go/compose/credentials.go +++ b/go/compose/credentials.go @@ -37,11 +37,9 @@ const ( // beside them (novox/hq ADR 0225); one whose resolution binds it to another provider is withdrawn and returned beside them // (issue 274). Every other is granted. func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, []string, error) { - var issued []Issued - for _, row := range c.in.Issued { - if row.Provider == node { - issued = append(issued, row) - } + issued, err := c.rowsFor(node) + if err != nil { + return nil, nil, nil, nil, err } onNetwork, err := c.whereEveryoneIs() if err != nil { @@ -114,6 +112,61 @@ func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overfl return out, withheld, unbound, foreseen, nil } +// 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 +// would make before it composes this node -- every need of another node that resolves, whose provider is this node, that is not +// answered by a record and not a credential only a person gives and nobody gave. Those have no sealed copy (ForProvider empty), so the +// grant is composed with a stand-in and named foreseen (plan.go: planFor makes the secret in every mode before grantsFor reads it). +func (c *composer) rowsFor(node string) ([]Issued, error) { + var rows []Issued + for _, row := range c.in.Issued { + if row.Provider == node { + rows = append(rows, row) + } + } + for _, other := range c.in.sorted() { + if other.Name == node { + continue + } + resolved, err := c.planFor(other.Name) + switch { + case unresolvable(err): + continue + case err != nil: + return nil, fmt.Errorf("what %s asked of %s cannot be read: %w", other.Name, node, err) + } + shelf := c.shelfFor(other) + for _, n := range resolved.Plan.Needs { + if n.From != node || n.ByRecord { + continue + } + cred, held := c.credential(n, other.Name) + if _, outside := issuedOutsideBy(shelf, n); cred.State == CredentialNotGiven || (outside && !(held && cred.State == CredentialHeld)) { + continue + } + if slices.ContainsFunc(rows, func(r Issued) bool { + return r.Provision == n.Name && r.Consumer == other.Name && r.ConsumerModule == n.For && r.Local == n.Local + }) { + continue + } + rows = append(rows, Issued{Provision: n.Name, Consumer: other.Name, ConsumerModule: n.For, Local: n.Local, Provider: node}) + } + } + sort.SliceStable(rows, func(i, j int) bool { + a, b := rows[i], rows[j] + switch { + case a.Provision != b.Provision: + return a.Provision < b.Provision + 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 + }) + return rows, nil +} + // boundOfGrant is the identity bound the consumer's own resolution states for the requirement this grant answers. A requirement // not found there is held to the tightest bound the mesh knows rather than to none. func boundOfGrant(consumer catalogue.Resolution, s Issued, provider string) catalogue.IdentityBound { diff --git a/go/compose/credentials_test.go b/go/compose/credentials_test.go index 2db33ed..4c82137 100644 --- a/go/compose/credentials_test.go +++ b/go/compose/credentials_test.go @@ -365,3 +365,62 @@ 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 +// carries its grant, composed with a stand-in and named foreseen. +func TestAConsumerNoCredentialWasMadeForYetIsGrantedWithAStandInAndNamedForeseen(t *testing.T) { + in := aGrantMesh(t) + in.Issued = nil + got := rendered(t, in, "anchor", Foreseeing) + if len(got.Rendering.Grants) != 1 { + t.Fatalf("grants are %+v", got.Rendering.Grants) + } + g := got.Rendering.Grants[0] + if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.Sealed != catalogue.ForeseenSealed { + t.Errorf("the grant is %+v", g) + } + if !has(got.Foreseen, "the grant of s3-bucket for files on laptop") { + t.Errorf("foreseen %v", got.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.Error("the consumer is granted its own credential") + } + if len(in.Issued) != 0 { + t.Error("a row was added to the Input") + } + // A credential only a person gives and nobody has is no grant. + 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 { + t.Errorf("a credential nobody gave is granted: %+v", got.Rendering.Grants) + } +} + +// And a consumer whose identity overflows the provision's bound is judged before any credential is made for it (issue 263). +func TestAnOverflowingConsumerNoCredentialWasMadeForYetIsWithheld(t *testing.T) { + in := aGrantMesh(t) + in.Issued = nil + in.Catalogue["resolver"] = storeManifests()[1] + in.Catalogue["networkmanager"] = catalogue.Manifest{Module: "networkmanager", Version: "1", Requires: []string{"wildcard-resolution", "s3-bucket"}} + nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "networkmanager") + nodeOf(t, &in, "anchor").Assigned = append(nodeOf(t, &in, "anchor").Assigned, "resolver") + in.Holdings = []catalogue.Held{{Claim: "mesh-dns-resolver", Scope: catalogue.ScopeMesh, Node: "anchor", Module: "resolver", Site: "here"}} + got := rendered(t, in, "anchor", Foreseeing) + if len(got.Withheld) != 1 || got.Withheld[0].Module != "networkmanager" { + t.Fatalf("withheld %+v", got.Withheld) + } +} + +// The grants come in the order the controller reads them, whatever order the Input gives its rows. +func TestGrantsComeInTheOrderOfProvisionConsumerModuleAndLocal(t *testing.T) { + in := aGrantMesh(t) + in.Issued = []Issued{ + {Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Local: "b", Provider: "anchor", ForProvider: "x"}, + {Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Local: "a", Provider: "anchor", ForProvider: "x"}, + } + rows, err := newComposer(in).rowsFor("anchor") + // The credential the same send would make (no local name) comes first, then the two the Input gives, a before b. + if err != nil || len(rows) != 3 || rows[0].Local != "" || rows[1].Local != "a" || rows[2].Local != "b" { + t.Errorf("rows are %+v %v", rows, err) + } +}