Files
zip/statedcaller_test.go
T
zeekayandHanzo Dev 5835826a43 caller: forward the whole identity, and let a background caller state one
identityHeaders forwarded five of the nine headers a gateway asserts, so a
callee reached over the op-call plane saw a principal with no project, no
minted name, no owner and no org-admin bit — while the same handler reached
over REST saw all four. Two of them decide authority: owner carries membership
of a reserved platform org, and isOrgAdmin says only that someone administers
their own org. A subset let a callee read the second for the first, or find
neither, without anything on either side saying so.

Add WithCaller for the caller that has no inbound request to propagate. A
grant issued when an org opens and a meter that debits after the response has
gone out both act FOR a tenant; without a way to say which, the only place left
for the org is the argument, and an org in the argument is one the caller chose.
An inbound request still wins, so this supplies an identity where there is none
and cannot override one. CallerOf reads it back off the same context, so a job
sees what it is about to send.

Ctx gains Project, UserName, UserOwner and IsOrgAdmin, so the REST reader and
the call reader spell one vocabulary.

TestForwardedIdentityCrossesWhole fails on exactly the four dropped fields if
the array is narrowed again; the other three cover the stated path, the two
admin scopes staying apart, and an unattributed call staying anonymous.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 21:28:18 -07:00

172 lines
5.6 KiB
Go

package zip_test
import (
"context"
"encoding/json"
"net/http/httptest"
"testing"
"github.com/zap-proto/zip"
)
// The identity plane has one job: the principal a callee acts on is the same
// one whether the request arrived over REST or over a call, and no caller can
// promote itself along the way. These exercise the three ways that can break.
type whoIn struct{}
type whoOut struct {
Org string `json:"org"`
Project string `json:"project"`
User string `json:"user"`
Name string `json:"name"`
Email string `json:"email"`
Owner string `json:"owner"`
Admin bool `json:"admin"`
OrgAdmin bool `json:"orgAdmin"`
}
func whoApp(t *testing.T) *zip.App {
t.Helper()
app := zip.New(zip.Config{AppName: "who"})
zip.Post[whoIn, whoOut](app, "/v1/who", func(ctx context.Context, _ *whoIn) (*whoOut, error) {
c := zip.CallerOf(ctx)
return &whoOut{
Org: c.Org, Project: c.Project, User: c.User, Name: c.Name,
Email: c.Email, Owner: c.Owner, Admin: c.Admin, OrgAdmin: c.OrgAdmin,
}, nil
}, zip.WithOperationID("who_ask"))
return app
}
// dialWho serves the app on its own unix socket and dials it back.
func dialWho(t *testing.T) *zip.Conn {
t.Helper()
c, err := zip.Dial(serveUDS(t, whoApp(t)))
if err != nil {
t.Fatalf("dial: %v", err)
}
t.Cleanup(func() { _ = c.Close() })
return c
}
// A stated caller reaches the callee WHOLE. The subset bug is the dangerous
// one: a plane that forwarded org and dropped owner would have every callee
// decide privilege differently over a call than over REST, and nothing would
// say so.
func TestStatedCallerCrossesWhole(t *testing.T) {
conn := dialWho(t)
want := zip.Caller{
Org: "acme", Project: "proj-1", User: "u-7", Name: "ada",
Email: "[email protected]", Owner: "acme", Admin: true, OrgAdmin: true,
}
ctx := zip.WithCaller(context.Background(), want)
got, err := zip.Call[whoIn, whoOut](ctx, conn, "who_ask", &whoIn{})
if err != nil {
t.Fatalf("call: %v", err)
}
if got.Org != want.Org || got.Project != want.Project || got.User != want.User ||
got.Name != want.Name || got.Email != want.Email || got.Owner != want.Owner ||
!got.Admin || !got.OrgAdmin {
t.Fatalf("identity did not cross whole:\n got %+v\nwant %+v", got, want)
}
}
// The two admin scopes must not bleed into one another. An org admin of their
// own org is not platform sudo, and a plane that forwarded IsOrgAdmin into
// IsAdmin (or inferred either from the other) would hand a tenant's own admin
// the cross-tenant surfaces.
func TestOrgAdminIsNotPlatformAdmin(t *testing.T) {
conn := dialWho(t)
ctx := zip.WithCaller(context.Background(), zip.Caller{Org: "acme", Owner: "acme", OrgAdmin: true})
got, err := zip.Call[whoIn, whoOut](ctx, conn, "who_ask", &whoIn{})
if err != nil {
t.Fatalf("call: %v", err)
}
if got.Admin {
t.Fatal("an org admin arrived as a platform admin")
}
if !got.OrgAdmin {
t.Fatal("the org-admin scope was dropped")
}
if got.Owner != "acme" {
t.Fatalf("owner = %q, want acme", got.Owner)
}
}
// A stated caller reads back off its own context. Write and read are the same
// value, so a background job sees the identity it is about to send rather than
// discovering it one hop later.
func TestStatedCallerReadsBack(t *testing.T) {
ctx := zip.WithCaller(context.Background(), zip.Caller{Org: "acme", User: "u-7"})
if c := zip.CallerOf(ctx); c.Org != "acme" || c.User != "u-7" {
t.Fatalf("CallerOf = %+v, want org acme user u-7", c)
}
if c := zip.CallerOf(context.Background()); c != (zip.Caller{}) {
t.Fatalf("a bare context claimed an identity: %+v", c)
}
}
// The FORWARDED path — a real inbound request, propagated onward with
// Ctx.Forward. This is the one the identityHeaders list governs, and the one a
// narrowed list breaks: drop a header from that array and the callee decides on
// a principal missing exactly the field that was dropped.
func TestForwardedIdentityCrossesWhole(t *testing.T) {
callee := dialWho(t)
front := zip.New(zip.Config{AppName: "front"})
front.Get("/ask", func(c *zip.Ctx) error {
out, err := zip.Call[whoIn, whoOut](c.Forward(), callee, "who_ask", &whoIn{})
if err != nil {
return err
}
return c.JSON(200, out)
})
req := httptest.NewRequest("GET", "/ask", nil)
for h, v := range map[string]string{
zip.HeaderOrg: "acme", zip.HeaderProject: "proj-1",
zip.HeaderUser: "u-7", zip.HeaderUserName: "ada",
zip.HeaderUserEmail: "[email protected]", zip.HeaderUserOwner: "acme",
zip.HeaderUserAdmin: "true", zip.HeaderUserOrgAdmin: "true",
} {
req.Header.Set(h, v)
}
resp, err := front.Fiber().Test(req)
if err != nil {
t.Fatalf("test request: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != 200 {
t.Fatalf("status %d", resp.StatusCode)
}
var got whoOut
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatalf("decode: %v", err)
}
want := whoOut{
Org: "acme", Project: "proj-1", User: "u-7", Name: "ada",
Email: "[email protected]", Owner: "acme", Admin: true, OrgAdmin: true,
}
if got != want {
t.Fatalf("forwarded identity lost fields:\n got %+v\nwant %+v", got, want)
}
}
// Nothing stated: nothing sent. An unattributed call must look unattributed —
// a callee that admits an empty org because it read one that was never
// asserted is the failure this preserves against.
func TestUnstatedCallerStaysAnonymous(t *testing.T) {
conn := dialWho(t)
got, err := zip.Call[whoIn, whoOut](context.Background(), conn, "who_ask", &whoIn{})
if err != nil {
t.Fatalf("call: %v", err)
}
if got.Org != "" || got.User != "" || got.Owner != "" || got.Admin || got.OrgAdmin {
t.Fatalf("an unattributed call carried an identity: %+v", got)
}
}