mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
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.
98 lines
3.7 KiB
Go
98 lines
3.7 KiB
Go
// Copyright (c) 2014-present Hanzo AI, Inc.
|
|
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
|
|
|
|
package store
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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"
|
|
)
|
|
|
|
// newStoreAPI wires the store CRUD exactly as the /v1 bundle does — the
|
|
// gateway-trust IAMTokenRequired group middleware (which turns the minted
|
|
// X-Org-Id / X-User-IsAdmin headers into the request's org + permissions) in
|
|
// front of storeApi.Route(api, tokenRequired). This is the real gate POST
|
|
// /v1/store passes through.
|
|
func newStoreAPI(ctx ae.Context) *zipclient.Client {
|
|
cl := zipclient.New(ctx)
|
|
cl.Use(iammiddleware.IAMTokenRequired())
|
|
Route(cl.Router, middleware.TokenRequired())
|
|
return cl
|
|
}
|
|
|
|
func postStore(cl *zipclient.Client, headers map[string]string) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodPost, "/store", strings.NewReader(`{"name":"onboarding-store"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
return cl.Do(req)
|
|
}
|
|
|
|
// TestCreateStore_OrgAdminOwnOrg is onboarding step 1: an edge-verified ORG
|
|
// admin (X-User-IsOrgAdmin=="true") creating a store in ITS OWN org (X-Org-Id) must
|
|
// succeed. Before the fix the org owner's permissions carried no WriteStore bit,
|
|
// so rest.CheckPermissions("create") 403'd and self-serve onboarding died here.
|
|
func TestCreateStore_OrgAdminOwnOrg(t *testing.T) {
|
|
ctx := ae.NewContext()
|
|
defer ctx.Close()
|
|
cl := newStoreAPI(ctx)
|
|
|
|
w := postStore(cl, map[string]string{
|
|
iammiddleware.HeaderUserIsOrgAdmin: "true",
|
|
iammiddleware.HeaderUserOwner: "acme", // HOME org
|
|
"X-Org-Id": "acme", // EFFECTIVE org — equal, so the grant applies
|
|
"X-User-Id": "user-acme-owner",
|
|
})
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("org admin create store in own org: got %d, want 201\nbody: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestCreateStore_NonAdminRefused: an authenticated principal that is NOT an org
|
|
// admin (no X-User-IsAdmin) holds no WriteStore bit, so it cannot create a store
|
|
// even in its own org.
|
|
func TestCreateStore_NonAdminRefused(t *testing.T) {
|
|
ctx := ae.NewContext()
|
|
defer ctx.Close()
|
|
cl := newStoreAPI(ctx)
|
|
|
|
w := postStore(cl, map[string]string{
|
|
"X-Org-Id": "acme",
|
|
"X-User-Id": "user-acme-member",
|
|
})
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("non-admin create store: got %d, want 403\nbody: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestCreateStore_CrossOrgRefused proves the grant can never cross tenants — the
|
|
// ADVERSARIAL case: the caller carries X-User-IsOrgAdmin=="true" AND a HOME org
|
|
// (X-User-Owner="acme") but the EFFECTIVE org (X-Org-Id="other") is a DIFFERENT
|
|
// tenant. Commerce must NOT apply orgAdminGrant when home != effective (it enforces
|
|
// the coupling itself via orgAdminHomeMatches, rather than trusting the gateway's
|
|
// binding by comment), so the create is refused — an org-switched principal can
|
|
// never gain merchant authority over a foreign org.
|
|
func TestCreateStore_CrossOrgRefused(t *testing.T) {
|
|
ctx := ae.NewContext()
|
|
defer ctx.Close()
|
|
cl := newStoreAPI(ctx)
|
|
|
|
w := postStore(cl, map[string]string{
|
|
iammiddleware.HeaderUserIsOrgAdmin: "true", // org-admin flag present…
|
|
iammiddleware.HeaderUserOwner: "acme", // …of the HOME org acme…
|
|
"X-Org-Id": "other", // …but acting in a DIFFERENT (victim) org
|
|
"X-User-Id": "user-acme-owner",
|
|
})
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("cross-org create store (home!=effective): got %d, want 403\nbody: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|