FLEX-WP-0035-T05: informed-decision lifecycle v2 declares application.access
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m5s

- application.access allows only with purpose=readback and the authoring
  human bar; no workload path, no audience or client-scoped claims
- v1 operations unchanged; pin.json moves to v2; live pin stays v1 until T04
- examples/application-readback: readback catalogue with informed-decision's
  key-cape client binding; integration test over the shipped files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 609675@bnt-lap001
Assistant-Session: 4987a36e-eb38-4b10-803c-15a29cd78212
This commit is contained in:
tegwick 2026-10-09 20:10:50 +02:00
parent a4103272e1
commit b5da745063
10 changed files with 207 additions and 12 deletions

View file

@ -12,6 +12,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/netkingdom/flex-auth/internal/audit"
"github.com/netkingdom/flex-auth/internal/callerauth"
@ -298,3 +299,47 @@ func TestReadbackShapesMatchSchemas(t *testing.T) {
}
}
}
// FLEX-WP-0035-T05: the shipped catalogue and informed-decision lifecycle v2
// answer the readback the way user-engine will ask it.
func TestReadbackInformedDecisionCatalogue(t *testing.T) {
examples := filepath.Join("..", "..", "examples")
service, err := buildReadback(context.Background(),
[]string{
filepath.Join(examples, "application-readback", "registry_snapshot.json"),
filepath.Join(examples, "informed-decision-lifecycle", "registry.json"),
},
map[string]string{"informed-decision": filepath.Join(examples, "informed-decision-lifecycle", "policy.md")})
if err != nil {
t.Fatal(err)
}
subject := func(groups ...any) readback.Request {
return readback.Request{
Tenant: "tenant:platform",
Subject: api.SubjectRef{ID: "human-one", Type: "human", Tenant: "tenant:platform", Attributes: map[string]any{
"issuer": "https://kc.coulomb.social", "roles": []any{}, "tenant_roles": []any{}, "scopes": []any{}, "groups": groups,
"assurance": map[string]any{"level": "aal2", "mfa": true, "source": "key-cape", "methods": []any{"pwd", "otp"}, "at": time.Now().Unix()},
}},
Systems: []string{"informed-decision"},
}
}
for name, tc := range map[string]struct {
request readback.Request
effect api.DecisionEffect
}{
"admin": {subject("net-kingdom-admins"), api.DecisionEffectAllow},
"non-admin": {subject("staff"), api.DecisionEffectDeny},
} {
response, err := service.Evaluate(context.Background(), tc.request)
if err != nil {
t.Fatal(err)
}
if got := response.Results[0]; got.Effect != tc.effect || got.PolicyPackage != "informed-decision.lifecycle" {
t.Fatalf("%s: %+v", name, got)
}
}
systems := service.Systems().Systems
if len(systems) != 1 || !systems[0].Readback || systems[0].ClientBinding == nil || systems[0].ClientBinding.ClientID != "informed-decision-approver" {
t.Fatalf("catalogue = %+v", systems)
}
}