2026-09-05 00:41:17 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"keycape/internal/config"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestServiceRegistrationAudienceAndScopeIsolation(t *testing.T) {
|
|
|
|
|
cfg, err := config.Load("../../../config/service-clients.example.yaml")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, c := range cfg.Clients {
|
|
|
|
|
t.Setenv(c.SecretRef[4:], "test-only-secret")
|
|
|
|
|
}
|
|
|
|
|
registry, err := buildClientRegistry(cfg.Clients)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, id := range []string{"secrets-engine-approval", "approval-engine-operator"} {
|
|
|
|
|
c := registry[id]
|
|
|
|
|
if c == nil || c.Audience != "approval-engine" || c.TokenLifetime != 15*time.Minute {
|
|
|
|
|
t.Fatalf("invalid registration for %s", id)
|
|
|
|
|
}
|
|
|
|
|
for _, scope := range c.AllowedScopes {
|
|
|
|
|
if id == "approval-engine-operator" && scope == "approval:consume" {
|
|
|
|
|
t.Fatal("operator may not consume")
|
|
|
|
|
}
|
|
|
|
|
if id == "secrets-engine-approval" && scope != "approval:read" && scope != "approval:consume" {
|
|
|
|
|
t.Fatal("excess PEP scope")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if registry["secrets-engine-openbao"].Audience != "" {
|
|
|
|
|
t.Fatal("OpenBao audience default changed")
|
|
|
|
|
}
|
|
|
|
|
}
|
Align approval registrations to the tenant:platform decision
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
2026-09-06 22:30:32 +02:00
|
|
|
|
|
|
|
|
// The Glas approval chain requires exact tenant spelling across the approval
|
|
|
|
|
// store, these JWT claims and the lifecycle CheckRequest (decision
|
|
|
|
|
// 5ed3fb35-eca9-413a-82b9-95171ba85bf6). Aliases to "platform" or
|
|
|
|
|
// "tenant:coulomb" are rejected, and unrelated clients keep their own tenant.
|
|
|
|
|
func TestServiceRegistrationTenantsAreExactPerDecision(t *testing.T) {
|
|
|
|
|
cfg, err := config.Load("../../../config/service-clients.example.yaml")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
want := map[string]string{
|
|
|
|
|
"secrets-engine-approval": "tenant:platform",
|
|
|
|
|
"approval-engine-operator": "tenant:platform",
|
|
|
|
|
"codex-railiance-platform": "tenant:coulomb",
|
|
|
|
|
"secrets-engine-openbao": "tenant:coulomb",
|
|
|
|
|
}
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
for _, c := range cfg.Clients {
|
|
|
|
|
expected, ok := want[c.ClientID]
|
|
|
|
|
if !ok {
|
|
|
|
|
t.Fatalf("unreviewed client %s has tenant %q", c.ClientID, c.Tenant)
|
|
|
|
|
}
|
|
|
|
|
if c.Tenant != expected {
|
|
|
|
|
t.Fatalf("%s: tenant %q, want exactly %q", c.ClientID, c.Tenant, expected)
|
|
|
|
|
}
|
|
|
|
|
seen[c.ClientID] = true
|
|
|
|
|
}
|
|
|
|
|
for id := range want {
|
|
|
|
|
if !seen[id] {
|
|
|
|
|
t.Fatalf("missing reviewed registration %s", id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|