Files
commerce/middleware/accesstoken_test.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

110 lines
4.1 KiB
Go

// Copyright (c) 2014-present Hanzo AI, Inc.
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/util/bit"
"github.com/hanzoai/commerce/util/permission"
)
// runGate drives TokenRequired(masks...) through a real zip app with the given
// pre-set context state (iam_authenticated / permissions), returning the HTTP
// status and whether the protected handler was reached. COMMERCE_SERVICE_TOKEN
// is cleared so only the IAM branch under test is exercised.
func runGate(t *testing.T, masks []bit.Mask, seed func(*zip.Ctx)) (int, bool) {
t.Helper()
reached := false
app := zip.New(zip.Config{DisableStartupMessage: true})
app.Use(zip.H(func(c *zip.Ctx) error { seed(c); return c.Next() }))
app.Use(TokenRequired(masks...))
app.Raw(http.MethodPost, "/x", func(c *zip.Ctx) error { reached = true; return c.NoContent(http.StatusOK) })
resp, err := app.Test(httptest.NewRequest(http.MethodPost, "/x", nil))
if err != nil {
t.Fatalf("request failed: %v", err)
}
return resp.StatusCode, reached
}
func iamAuth(perms bit.Field) func(*zip.Ctx) {
return func(c *zip.Ctx) {
c.Locals("iam_authenticated", true)
c.Locals("permissions", perms)
}
}
// TestTokenRequired_IAMBranchEnforcesMasks is the core fix-2 proof: the IAM
// branch used to bare-c.Next() for ANY authenticated principal, which made every
// masked gate (TokenRequired(Admin, Published) on the checkout money routes) a
// NO-OP — a low-privilege or forged (perms=0) IAM caller reached the money
// handlers. It now enforces the requested masks against the gateway-minted
// permissions (bit.Field.Has = intersection, so Admin|Published is satisfied by
// EITHER bit). A no-mask gate (billing) still admits any authenticated principal.
func TestTokenRequired_IAMBranchEnforcesMasks(t *testing.T) {
published := []bit.Mask{permission.Admin, permission.Published}
cases := []struct {
name string
masks []bit.Mask
perms bit.Field
wantStatus int
wantReach bool
}{
{"admin mints (Admin|Live intersects Admin)", published,
bit.Field(permission.Admin | permission.Live), http.StatusOK, true},
{"published storefront principal mints", published,
bit.Field(permission.Published | permission.Live), http.StatusOK, true},
{"forged/low-priv perms=0 denied", published,
bit.Field(0), http.StatusForbidden, false},
{"read-only scope denied a mint", published,
bit.Field(permission.ReadStore), http.StatusForbidden, false},
{"no-mask gate admits any authed principal (billing)", nil,
bit.Field(0), http.StatusOK, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
status, reached := runGate(t, tc.masks, iamAuth(tc.perms))
if status != tc.wantStatus || reached != tc.wantReach {
t.Fatalf("status=%d reached=%v, want status=%d reached=%v",
status, reached, tc.wantStatus, tc.wantReach)
}
})
}
}
// TestTokenRequired_BareOrgHeaderIsNotAuthenticated proves the IsIAMAuthenticated
// hardening end-to-end: a request carrying ONLY a client X-Org-Id header (no
// validated iam_authenticated, no valid token, no service token) is NOT treated
// as an IAM principal — it falls through to token auth and 401s. Before the fix,
// the bare header alone satisfied IsIAMAuthenticated and reached the handler.
func TestTokenRequired_BareOrgHeaderIsNotAuthenticated(t *testing.T) {
reached := false
app := zip.New(zip.Config{DisableStartupMessage: true})
app.Use(TokenRequired(permission.Admin, permission.Published))
app.Raw(http.MethodPost, "/x", func(c *zip.Ctx) error {
reached = true
return c.NoContent(http.StatusOK)
})
req := httptest.NewRequest(http.MethodPost, "/x", nil)
req.Header.Set("X-Org-Id", "hanzo") // spoofed org selector, no token behind it
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
if resp.StatusCode != http.StatusUnauthorized || reached {
t.Fatalf("bare X-Org-Id must not authenticate: status=%d reached=%v, want 401,false", resp.StatusCode, reached)
}
}