Accept both PDP token audiences while preserving caller bindings
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
3d44f0f9d9
commit
21d4e20696
5 changed files with 329 additions and 6 deletions
131
cmd/access-engine/runtime_rename_test.go
Normal file
131
cmd/access-engine/runtime_rename_test.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/netkingdom/access-engine/internal/callerauth"
|
||||
"github.com/netkingdom/access-engine/internal/policy"
|
||||
"github.com/netkingdom/access-engine/pkg/api"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// The packages name their protected consumer, not the PDP. Authenticate both
|
||||
// PDP audiences against exactly the installed mapping, then compare real engine
|
||||
// outcomes and canonical request bindings with the unchanged package baseline.
|
||||
func TestRuntimeRenamePinPackagesPreserveDecisionMaterial(t *testing.T) {
|
||||
for _, pin := range []string{"ops-warden", "ops-warden-fetch", "secrets-engine", "user-engine", "tenant-engine", "informed-decision-lifecycle", "informed-decision-sitting", "informed-decision-t03"} {
|
||||
t.Run(pin, func(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "values", pin+".yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var values struct {
|
||||
Args []string `yaml:"args"`
|
||||
CallerAuth struct {
|
||||
Binding string `yaml:"binding"`
|
||||
Extra []string `yaml:"extraBindings"`
|
||||
} `yaml:"callerAuth"`
|
||||
}
|
||||
if err := yaml.Unmarshal(raw, &values); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bindings := map[string]string{}
|
||||
for _, entry := range append([]string{values.CallerAuth.Binding}, values.CallerAuth.Extra...) {
|
||||
system, principal, ok := strings.Cut(entry, "=")
|
||||
if !ok {
|
||||
t.Fatalf("bad mapping: %s", entry)
|
||||
}
|
||||
bindings[system] = principal
|
||||
}
|
||||
paths := map[string]string{}
|
||||
for i := 0; i+1 < len(values.Args); i++ {
|
||||
if values.Args[i] == "--registry" || values.Args[i] == "--policy" {
|
||||
paths[values.Args[i]] = filepath.Join("..", "..", strings.TrimPrefix(values.Args[i+1], "/opt/flex-auth/"))
|
||||
}
|
||||
}
|
||||
pkg, err := policy.LoadFile(paths["--policy"])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
engine, err := buildEngine(context.Background(), paths["--registry"], paths["--policy"], "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pkg.Fixtures) == 0 {
|
||||
t.Fatal("package has no decision fixtures")
|
||||
}
|
||||
exercised := 0
|
||||
for _, fixture := range pkg.Fixtures {
|
||||
principal, installed := bindings[fixture.Request.Resource.System]
|
||||
if !installed {
|
||||
continue
|
||||
}
|
||||
baseline, err := engine.Check(context.Background(), fixture.Request)
|
||||
if err != nil {
|
||||
t.Fatalf("baseline %s: %v", fixture.ID, err)
|
||||
}
|
||||
original := api.CanonicalDigest(fixture.Request)
|
||||
for _, audience := range []string{"flex-auth", "access-engine"} {
|
||||
a, err := callerauth.New(callerauth.ModeEnforce, fixedTokenReviewer{identity: callerauth.Identity{Username: principal, Audiences: []string{audience}}}, "flex-auth", bindings, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identity, err := a.Authorize(context.Background(), "Bearer test-token", []string{fixture.Request.Resource.System})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := engine.Check(callerauth.WithRecord(context.Background(), a.Record(identity)), fixture.Request)
|
||||
if err != nil {
|
||||
t.Fatalf("%s/%s: %v", fixture.ID, audience, err)
|
||||
}
|
||||
if got.Effect != baseline.Effect || got.Reason != baseline.Reason || got.Binding.RequestDigest != baseline.Binding.RequestDigest {
|
||||
t.Fatalf("%s/%s decision material changed", fixture.ID, audience)
|
||||
}
|
||||
if got.Provenance.Caller.Audience != audience {
|
||||
t.Fatal("requested audience spelling lost")
|
||||
}
|
||||
if got.Resource.System != fixture.Request.Resource.System || got.Binding.Resource.System != fixture.Request.Resource.System {
|
||||
t.Fatal("resource.system spelling changed")
|
||||
}
|
||||
if api.CanonicalDigest(fixture.Request) != original {
|
||||
t.Fatal("original request silently rewritten")
|
||||
}
|
||||
}
|
||||
exercised++
|
||||
}
|
||||
if exercised == 0 {
|
||||
t.Fatal("no installed-system fixtures exercised")
|
||||
}
|
||||
t.Logf("%d owner-installed package fixtures preserved with both audiences", exercised)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRenameReadbackRoutesPreserveCallerBoundary(t *testing.T) {
|
||||
for _, audience := range []string{"flex-auth", "access-engine"} {
|
||||
for _, principal := range []string{readbackPrincipal, "system:serviceaccount:other:caller"} {
|
||||
a, err := callerauth.New(callerauth.ModeEnforce, fixedTokenReviewer{identity: callerauth.Identity{Username: principal, Audiences: []string{audience}}}, "flex-auth", map[string]string{readbackBinding: readbackPrincipal}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := httptest.NewServer(newReadbackMux(newTestReadback(t), a))
|
||||
valid, _ := json.Marshal(subjectRequest("user:alice", true, "demo-app"))
|
||||
got := postReadback(t, server.URL, string(valid)).StatusCode
|
||||
server.Close()
|
||||
want := http.StatusForbidden
|
||||
if principal == readbackPrincipal {
|
||||
want = http.StatusOK
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("%s/%s: status %d want %d", audience, principal, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +130,14 @@ func (a *Authenticator) Record(identity Identity) Record {
|
|||
return record
|
||||
}
|
||||
record.Principal = identity.Username
|
||||
record.Audience = audience
|
||||
// Stamp an audience actually returned by TokenReview, never merely the
|
||||
// configured audience. Prefer the configured spelling when both matched.
|
||||
for _, accepted := range runtimeAudiences(audience) {
|
||||
if contains(identity.Audiences, accepted) {
|
||||
record.Audience = accepted
|
||||
break
|
||||
}
|
||||
}
|
||||
record.NotAfter = identity.NotAfter
|
||||
return record
|
||||
}
|
||||
|
|
@ -153,7 +160,7 @@ func (a *Authenticator) authorize(ctx context.Context, authorization string, sys
|
|||
identity.NotAfter = exp
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(identity.Username) == "" || !contains(identity.Audiences, a.audience) {
|
||||
if strings.TrimSpace(identity.Username) == "" || !matchesRuntimeAudience(identity.Audiences, a.audience) {
|
||||
return Identity{}, ErrUnauthenticated
|
||||
}
|
||||
if len(systems) == 0 {
|
||||
|
|
@ -176,3 +183,26 @@ func contains(values []string, wanted string) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// runtimeAudiences is the bounded FLEX-WP-0036 compatibility window. It
|
||||
// changes only the PDP audience, never protected-system names or principals.
|
||||
// A custom configured audience retains its exact original meaning.
|
||||
func runtimeAudiences(configured string) []string {
|
||||
switch configured {
|
||||
case "flex-auth":
|
||||
return []string{"flex-auth", "access-engine"}
|
||||
case "access-engine":
|
||||
return []string{"access-engine", "flex-auth"}
|
||||
default:
|
||||
return []string{configured}
|
||||
}
|
||||
}
|
||||
|
||||
func matchesRuntimeAudience(reviewed []string, configured string) bool {
|
||||
for _, accepted := range runtimeAudiences(configured) {
|
||||
if contains(reviewed, accepted) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
147
internal/callerauth/runtime_rename_test.go
Normal file
147
internal/callerauth/runtime_rename_test.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package callerauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestRuntimeRenameEveryInstalledPinBinding(t *testing.T) {
|
||||
pins := []string{"ops-warden", "ops-warden-fetch", "secrets-engine", "user-engine", "tenant-engine", "informed-decision-lifecycle", "informed-decision-sitting", "informed-decision-t03", "application-readback"}
|
||||
for _, pin := range pins {
|
||||
t.Run(pin, func(t *testing.T) {
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "values", pin+".yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var value struct {
|
||||
CallerAuth struct {
|
||||
Binding string `yaml:"binding"`
|
||||
Extra []string `yaml:"extraBindings"`
|
||||
} `yaml:"callerAuth"`
|
||||
Readback struct {
|
||||
Caller string `yaml:"caller"`
|
||||
} `yaml:"readback"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bindings := map[string]string{}
|
||||
for _, entry := range append([]string{value.CallerAuth.Binding}, value.CallerAuth.Extra...) {
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
system, principal, ok := strings.Cut(entry, "=")
|
||||
if !ok {
|
||||
t.Fatalf("bad binding: %s", entry)
|
||||
}
|
||||
bindings[system] = principal
|
||||
}
|
||||
// Readback's bounded authenticated caller is installed by serve-readback,
|
||||
// rather than a callerAuth binding flag.
|
||||
if value.Readback.Caller != "" {
|
||||
bindings["application-readback"] = value.Readback.Caller
|
||||
}
|
||||
if len(bindings) == 0 {
|
||||
t.Fatal("pin has no installed caller mapping")
|
||||
}
|
||||
for system, principal := range bindings {
|
||||
for _, configured := range []string{"flex-auth", "access-engine"} {
|
||||
for _, reviewed := range []string{"flex-auth", "access-engine"} {
|
||||
a, err := New(ModeEnforce, fakeReviewer{identity: Identity{Username: principal, Audiences: []string{reviewed}}}, configured, bindings, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, err := a.Authorize(context.Background(), "Bearer test-token", []string{system})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := a.Record(id).Audience; got != reviewed {
|
||||
t.Fatalf("provenance %q != reviewed %q", got, reviewed)
|
||||
}
|
||||
for _, forbidden := range []string{"foreign-consumer", "flex-auth", "access-engine"} {
|
||||
if _, err := a.Authorize(context.Background(), "Bearer test-token", []string{forbidden}); !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("%s accepted as %s: %v", system, forbidden, err)
|
||||
}
|
||||
}
|
||||
wrong, _ := New(ModeEnforce, fakeReviewer{identity: Identity{Username: "other-principal", Audiences: []string{reviewed}}}, configured, bindings, nil)
|
||||
if _, err := wrong.Authorize(context.Background(), "Bearer test-token", []string{system}); !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("wrong principal: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRenameUsesActualTokenReviewAudience(t *testing.T) {
|
||||
for _, configured := range []string{"flex-auth", "access-engine", "custom-pdp"} {
|
||||
for _, returned := range []string{"flex-auth", "access-engine", "foreign", "custom-pdp", ""} {
|
||||
t.Run(configured+"/"+returned, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
var request tokenReview
|
||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(request.Spec.Audiences, runtimeAudiences(configured)) {
|
||||
t.Errorf("requested audiences: %v", request.Spec.Audiences)
|
||||
}
|
||||
response := tokenReview{}
|
||||
response.Status.Authenticated = true
|
||||
response.Status.User.Username = "principal"
|
||||
if returned != "" {
|
||||
response.Status.Audiences = []string{returned}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer srv.Close()
|
||||
file := filepath.Join(t.TempDir(), "reviewer")
|
||||
if err := os.WriteFile(file, []byte("test-only-reviewer"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reviewer := &KubernetesTokenReviewer{endpoint: srv.URL, audience: configured, reviewerTokenFile: file, client: srv.Client()}
|
||||
a, _ := New(ModeEnforce, reviewer, configured, map[string]string{"consumer": "principal"}, nil)
|
||||
// The untrusted token claims the opposite: only authenticated TokenReview
|
||||
// response audiences decide admission and provenance.
|
||||
token := unsignedJWT(map[string]any{"aud": "foreign", "sub": "forged"})
|
||||
id, err := a.Authorize(context.Background(), "Bearer "+token, []string{"consumer"})
|
||||
want := contains(runtimeAudiences(configured), returned)
|
||||
if want {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.Record(id).Audience != returned {
|
||||
t.Fatal("configured/untrusted audience leaked into provenance")
|
||||
}
|
||||
} else if !errors.Is(err, ErrUnauthenticated) {
|
||||
t.Fatalf("foreign/missing audience accepted: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRenameCustomAudienceAndInputBindingsStayExact(t *testing.T) {
|
||||
bindings := map[string]string{"flex-auth": "principal"}
|
||||
a, err := New(ModeEnforce, fakeReviewer{identity: Identity{Username: "principal", Audiences: []string{"custom-pdp"}}}, "custom-pdp", bindings, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.Authorize(context.Background(), "Bearer test", []string{"access-engine"}); !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("uninstalled alias binding accepted: %v", err)
|
||||
}
|
||||
if len(bindings) != 1 {
|
||||
t.Fatal("input bindings mutated")
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ func (r *KubernetesTokenReviewer) Review(ctx context.Context, callerToken string
|
|||
Kind: "TokenReview",
|
||||
Spec: tokenReviewSpec{
|
||||
Token: callerToken,
|
||||
Audiences: []string{r.audience},
|
||||
Audiences: runtimeAudiences(r.audience),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ current source must now come from `./cmd/access-engine`.
|
|||
|
||||
```task
|
||||
id: FLEX-WP-0036-T03
|
||||
status: todo
|
||||
status: progress
|
||||
priority: high
|
||||
depends_on: [FLEX-WP-0036-T01]
|
||||
state_hub_task_id: "487525e5-c89e-586e-8e9b-54ac6da0804e"
|
||||
|
|
@ -126,8 +126,23 @@ and caller bindings accept both `resource.system` values. Decision records
|
|||
must still state which value the request carried, so nothing is rewritten
|
||||
silently. Add tests for both values on every pin's package.
|
||||
|
||||
Gate: tests; a new image rolled to the existing `flex-auth-*` pins with the
|
||||
founder's go.
|
||||
Gate: tests; a new image rolled to the existing `flex-auth-*` pins through
|
||||
reviewed reversible blue execution under
|
||||
`the-custodian/canon/standards/build-mode-change-authorization_v0.1.md`.
|
||||
The ordered live prerequisite must be verified before rollout.
|
||||
|
||||
2026-10-11 source preparation: bounded compatibility accepts both exact PDP
|
||||
audiences in Kubernetes TokenReview and records an actually reviewed spelling.
|
||||
Custom audiences, installed service principals and protected-consumer system
|
||||
bindings retain their existing meanings. All nine current pins authenticate both
|
||||
audiences; eight enforcement packages retain their fixture outcomes, canonical
|
||||
request bindings and original `resource.system` spelling. The ninth pin is the
|
||||
separate application-readback route, whose installed binding is
|
||||
`application-readback`, not a protected-system grant. There is no current
|
||||
PDP-self resource lane in these pin packages; the old inventory's
|
||||
`flex-auth-policy-check` example is a policy-routing lane outside this secret
|
||||
fetch package. No new self-lane or caller grant is created to fill that absence.
|
||||
T03 remains progress until the new image and ordered live checks are proven.
|
||||
|
||||
## Rename the source
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue