From 251b838c5e6a4bf9bc782dbd7aafb8a4d3ed32b6 Mon Sep 17 00:00:00 2001 From: CMGS Date: Mon, 17 Aug 2026 12:05:10 +0800 Subject: [PATCH] egress: a promoted-template claim takes the tenant policy alone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #81 made the effective policy a strict pool ∩ tenant intersection, and a key with no pool policy denies. A promoted template never has a pool — Promote and DeleteTemplate refuse pooled keys, and refill would build a golden for a name that is no image — so every promoted-template claim became default-deny regardless of the tenant's allow-list, closing the none lane's only route out for on-demand claims. Treat the missing pool layer as absent rather than empty: with no pool configured for the key, a tenant claim takes its own policy whole; a configured pool without a policy still grants nothing, and root claims of promoted templates stay denied for want of any layer. Closes #98. --- docs/egress.md | 4 +++- sandboxd/pool/egress.go | 10 +++++++--- sandboxd/pool/egress_test.go | 19 +++++++++++++------ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/egress.md b/docs/egress.md index 4627e70..b9436c5 100644 --- a/docs/egress.md +++ b/docs/egress.md @@ -102,7 +102,9 @@ A missing policy on either side is an empty allow-list, not a pass: a tenant without its own `egress` block reaches nothing — upgrading, a tenant that relied on inheriting its pool's policy must now declare one — so granting a tenant egress — and the secret injection that rides it — is always an explicit act. Root -claims have no tenant layer and take the pool's policy whole. Secrets are +claims have no tenant layer and take the pool's policy whole; a promoted +template has no pool layer, so a tenant's claim of one takes the tenant's +policy alone (a root claim of a promoted template stays denied). Secrets are registered separately and referenced by name — the value comes from the environment, never the config file. diff --git a/sandboxd/pool/egress.go b/sandboxd/pool/egress.go index 2544b2c..3b49744 100644 --- a/sandboxd/pool/egress.go +++ b/sandboxd/pool/egress.go @@ -183,18 +183,22 @@ func (m *Manager) poolIntercepts(key types.PoolKey) bool { return m.poolEgress[key].Intercepts() } -// effectivePolicy resolves pool ∩ tenant; root has no tenant layer. +// effectivePolicy resolves pool ∩ tenant; root has no tenant layer, and a +// promoted template — a key no pool is configured for — has no pool layer. func (m *Manager) effectivePolicy(sb *types.Sandbox) (egress.Evaluator, bool) { m.mu.Lock() defer m.mu.Unlock() poolPol := m.poolEgress[sb.Key] + tenantPol := m.tenantEgress[sb.Tenant] if poolPol == nil { - return nil, false + if m.pools[sb.Key] != nil || sb.Tenant == "" || tenantPol == nil { + return nil, false + } + return *tenantPol, true } if sb.Tenant == "" { return *poolPol, true } - tenantPol := m.tenantEgress[sb.Tenant] if tenantPol == nil { return nil, false } diff --git a/sandboxd/pool/egress_test.go b/sandboxd/pool/egress_test.go index 8b54780..c7f2366 100644 --- a/sandboxd/pool/egress_test.go +++ b/sandboxd/pool/egress_test.go @@ -140,19 +140,26 @@ func TestEffectivePolicyComposition(t *testing.T) { cases := []struct { name string tenant string + pooled bool pool, tnPol *egress.Policy allow, deny string wantArmed bool }{ - {"root takes the pool policy whole", "", both, nil, "a.test", "z.test", true}, - {"root without a pool policy", "", nil, nil, "", "", false}, - {"tenant intersects", "acme", both, tenantOnly, "b.test", "a.test", true}, // a.test allowed by pool, denied by tenant - {"tenant declaring no policy", "acme", both, nil, "", "", false}, - {"tenant on a policyless pool", "acme", nil, tenantOnly, "", "", false}, - {"neither", "acme", nil, nil, "", "", false}, + {"root takes the pool policy whole", "", true, both, nil, "a.test", "z.test", true}, + {"root without a pool policy", "", true, nil, nil, "", "", false}, + {"tenant intersects", "acme", true, both, tenantOnly, "b.test", "a.test", true}, // a.test allowed by pool, denied by tenant + {"tenant declaring no policy", "acme", true, both, nil, "", "", false}, + {"tenant on a policyless pool", "acme", true, nil, tenantOnly, "", "", false}, + {"tenant on a promoted template", "acme", false, nil, tenantOnly, "b.test", "a.test", true}, + {"root on a promoted template", "", false, nil, nil, "", "", false}, + {"neither", "acme", false, nil, nil, "", "", false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + m.pools = map[types.PoolKey]*pool{} + if tc.pooled { + m.pools[testKey] = &pool{key: testKey} + } m.poolEgress = map[types.PoolKey]*egress.Policy{} if tc.pool != nil { m.poolEgress[testKey] = tc.pool