Align approval registrations to the tenant:platform decision
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 33s

Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management tenant for the Glas approval chain, requiring exact
spelling across the approval store, the service-client JWT claim and the
lifecycle CheckRequest.

Changes the tenant field on secrets-engine-approval and approval-engine-operator
only, in the registration fixture and the provisioning packet. Unrelated clients
and the human directory default keep tenant:coulomb, and no audience, scope,
subject, role, lifetime or MFA grant changes.

Adds issuance evidence that the approval shape emits tenant:platform exactly and
never an alias the caller requests, that the OpenBao client gains no
cross-tenant reach, and a fixture guard pinning every reviewed client's tenant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
This commit is contained in:
tegwick 2026-09-06 22:30:32 +02:00
parent 519e4d8ef1
commit 7a6666d1f9
8 changed files with 199 additions and 28 deletions

View file

@ -104,3 +104,72 @@ func TestHumanTenantClaimUsesDirectoryValueThenPlatformDefault(t *testing.T) {
})
}
}
// Issuance evidence for the aligned approval chain: a token minted for the
// approval registrations carries tenant:platform exactly, and never the
// rejected alias spellings, even when the caller asks for them
// (decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6).
func TestApprovalClientIssuesExactPlatformTenantAndRejectsAliases(t *testing.T) {
h := serviceTokenHandler(t)
h.ClientConfig["secrets-engine-approval"] = &domain.Client{
ClientID: "secrets-engine-approval",
Audience: "approval-engine",
AllowedScopes: []string{"approval:read", "approval:consume"},
GrantTypes: []string{"client_credentials"},
ClientType: "confidential",
ClientSecret: "approval-test-secret",
ServiceSubject: "service:secrets-engine",
Tenant: "tenant:platform",
Roles: []string{"secrets-engine"},
}
for _, requested := range []string{"", "platform", "tenant:coulomb", "TENANT:PLATFORM"} {
values := url.Values{"grant_type": {"client_credentials"}, "scope": {"approval:read"}}
if requested != "" {
values.Set("tenant", requested)
values.Set("tenant_hint", requested)
}
req := tokenRequest(values)
req.SetBasicAuth("secrets-engine-approval", "approval-test-secret")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("requested %q: status %d", requested, w.Code)
}
token := decodeTokenResponse(t, w.Body.String())["access_token"].(string)
claims := parseJWTPayload(t, token)
if claims["tenant"] != "tenant:platform" {
t.Fatalf("requested %q: tenant %v, want tenant:platform", requested, claims["tenant"])
}
if claims["aud"] != "approval-engine" {
t.Fatalf("requested %q: audience %v", requested, claims["aud"])
}
verifyWithJWKS(t, h, token)
}
}
// The aligned decision grants no cross-tenant reach: the unrelated OpenBao
// login client keeps tenant:coulomb.
func TestUnrelatedServiceClientKeepsCoulombTenant(t *testing.T) {
h := serviceTokenHandler(t)
h.ClientConfig["secrets-engine-openbao"] = &domain.Client{
ClientID: "secrets-engine-openbao",
AllowedScopes: []string{"openbao:login"},
GrantTypes: []string{"client_credentials"},
ClientType: "confidential",
ClientSecret: "openbao-test-secret",
ServiceSubject: "service:secrets-engine",
Tenant: "tenant:coulomb",
Roles: []string{"secrets-engine"},
}
req := tokenRequest(url.Values{"grant_type": {"client_credentials"}, "scope": {"openbao:login"}, "tenant": {"tenant:platform"}})
req.SetBasicAuth("secrets-engine-openbao", "openbao-test-secret")
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status %d", w.Code)
}
token := decodeTokenResponse(t, w.Body.String())["access_token"].(string)
if got := parseJWTPayload(t, token)["tenant"]; got != "tenant:coulomb" {
t.Fatalf("tenant %v, want tenant:coulomb", got)
}
}