Files
commerce/api/invite/redeem_authz_test.go
zeekay f7aabf72f7 SuperAdmin is the identity edge's verdict, and commerce compares no org
IsSuperAdmin folded the case of the home org and fell back to the owner
claim, the org of the application that minted the token, and the money
bits were granted on X-User-Owner == admin. Cloud writes X-User-Owner for
an admin-org application and an admin-org person's API key as well as for
a person, so either could mint money and edit the catalog here while
being nobody in the rest of the same process. Every gate now reads
X-User-IsAdmin, which the edge strips and writes from authz.Claims.Sudo
alone; the standalone EdgeAuth decides it with that predicate over the
verified token's signed membership set. X-User-IsAdmin no longer doubles
as the org-admin bit, which is X-User-IsOrgAdmin. No machine is
SuperAdmin. Global admin is SuperAdmin wherever it named this predicate.
2026-10-18 22:08:47 -07:00

51 lines
1.8 KiB
Go

// Copyright (c) 2014-present Hanzo AI, Inc.
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
package invite
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
invitemodel "github.com/hanzoai/commerce/billing/invite"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/middleware/iammiddleware"
"github.com/hanzoai/commerce/util/test/ae"
"github.com/hanzoai/commerce/util/test/zipclient"
)
// TestRedeem_AdmitsOrgAdmin proves the paywall invite-redeem surface admits a
// gateway-verified ORG admin claiming a code for its OWN org — the way an org
// acquires access during onboarding. It is gated by the bundle's no-mask
// tokenRequired (any authenticated principal) behind IAMTokenRequired, so the org
// owner reaches Redeem and the code binds to its namespace (X-Org-Id), not a 403.
func TestRedeem_AdmitsOrgAdmin(t *testing.T) {
ctx := ae.NewContext()
defer ctx.Close()
// Platform mints the code out of band (Mint is superadmin-only).
if _, err := invitemodel.Mint(invitemodel.SystemDB(ctx), "WELCOME", ""); err != nil {
t.Fatalf("mint invite code: %v", err)
}
cl := zipclient.New(ctx)
cl.Use(iammiddleware.IAMTokenRequired())
Route(cl.Router, middleware.TokenRequired())
req := httptest.NewRequest(http.MethodPost, "/commerce/invite/redeem", strings.NewReader(`{"code":"WELCOME"}`))
req.Header.Set("Content-Type", "application/json")
req.Header.Set(iammiddleware.HeaderUserIsOrgAdmin, "true")
req.Header.Set("X-Org-Id", "acme")
req.Header.Set("X-User-Id", "user-acme-owner")
w := cl.Do(req)
if w.Code != http.StatusCreated {
t.Fatalf("org admin redeem invite: got %d, want 201\nbody: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), `"org":"acme"`) {
t.Fatalf("invite not bound to caller org: %s", w.Body.String())
}
}