Give a bundle module's bound credential to the account its runtime runs as (hq issue 624)
mesh/merge-gate out of date: judged against trunk f3875a6b6019, the trunk is now 0a1dc5f93fcb; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk f3875a6b6019, the trunk is now 0a1dc5f93fcb; judged again on a recheck or when it is merged
mesh/delivery stopped: the pull request closed unmerged

A consumer's credential file was composed with the owner the module declared, root when it declared
none. A module whose code is a bundle the node's runtime runs reads it as the operator's account, and
a root-owned 0600 file is one it cannot read: the lab cloned the forge with no credential. The grant
side was fixed the same way for issue 225; this is the consumer's side. A declared owner still wins.
This commit is contained in:
jochen
2026-10-13 14:39:20 +02:00
parent 584aa2d7d7
commit 41216e4d4c
2 changed files with 67 additions and 1 deletions
@@ -0,0 +1,57 @@
package catalogue
import "testing"
// A consumer's credential is read by the consumer's own code, and that stopped being root for every
// module whose code is a bundle the node's runtime runs (novox/hq issue 624, the consumer's side of
// issue 225). The lab requires the package registry and reads its bound credential to clone the forge;
// the credential file was composed with no owner, so root's and 0600, and the lab — running as the
// operator's account — cloned with no credential and was refused, "could not read Username".
// aConsumerWithABundle is the lab's shape: tools in a bundle, a requirement, its credential in a file.
func aConsumerWithABundle() Manifest {
return Manifest{
Module: "lab",
Requires: []string{"npm-package-registry"},
Secrets: map[string]string{"npm-package-registry": "/var/lib/lab/package-registry.secret"},
Bundles: []Bundle{{Name: "code", Language: "typescript"}},
}
}
func consumerSecret(t *testing.T, m Manifest, account string) map[string]any {
t.Helper()
r := Resolution{Node: "shanks", Account: account, Modules: []Manifest{m},
Needs: []Needed{{Name: "npm-package-registry", For: m.Module, From: "novox", Sealed: "c2VhbGVk"}}}
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
for _, res := range out {
if res["type"] == "file" && res["path"] == "/var/lib/lab/package-registry.secret" {
return res
}
}
t.Fatalf("no credential file was composed: %v", out)
return nil
}
func TestABundlesBoundCredentialBelongsToTheAccountItsRuntimeRunsAs(t *testing.T) {
if got := consumerSecret(t, aConsumerWithABundle(), "jochen")["owner"]; got != "jochen" {
t.Fatalf("the lab's credential belongs to %v; its tools run as %q, and a root-owned 0600 file is one "+
"they cannot read", got, "jochen")
}
}
// A module that names its secrets' owner keeps it, and one with no bundle and no owner stays root's.
func TestAConsumersDeclaredSecretsOwnerStillWins(t *testing.T) {
m := aConsumerWithABundle()
m.SecretsOwner = "65534:65534"
if got := consumerSecret(t, m, "jochen")["owner"]; got != "65534:65534" {
t.Errorf("a declared owner was replaced by %v", got)
}
m = aConsumerWithABundle()
m.Bundles = nil
if got, has := consumerSecret(t, m, "jochen")["owner"]; has {
t.Errorf("a container consumer's credential was given the owner %v", got)
}
}
+10 -1
View File
@@ -730,7 +730,16 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
// worse than none: something would read it and fail authenticating.
continue
}
first = append(first, ownedBy(m.SecretsOwner, map[string]any{
// **Read by the module's own code, so owned by whoever runs it** (novox/hq issue 624, the
// consumer's side of issue 225): a module whose code is a bundle the node's runtime runs reads
// its binding's credential as the operator's account, and a root-owned 0600 file is one it
// cannot read — the lab cloned the forge with no credential and said nothing about why. A
// module that names its secrets' owner keeps it.
owner := m.SecretsOwner
if owner == "" {
owner = r.provisionsAs(m)
}
first = append(first, ownedBy(owner, map[string]any{
"id": SecretID(SecretLocal(to, file.Local)), "type": "file", "path": file.Path,
"sealed": found.Sealed,
}))