mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
EdgeAuth set X-Org-Id, and pinned the billing subject, from the `owner` claim: the org of the application a person signed in through. Every self-serve user of the brand app therefore acted in and billed to the brand's org at a standalone commerce (COMMERCE_EDGE_AUTH=true). tenancy now resolves a verified token as cloud's SanitizeIdentity does: the home org is orgs[0], the acting org is the client's selection (X-Org-Id, or ?org on a billing path) when the signed set admits it, else the default (authz.Claims.EffectiveOrg). A program is its application's org and selects nothing; a person's token without a membership set acts in no org. The billing subject follows the acting org, and X-User-IsOrgAdmin is asked of it. orgAdminHomeMatches compared home and acting org with EqualFold; it compares them verbatim.
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
// Copyright (c) 2014-present Hanzo AI, Inc.
|
|
// Licensed under MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.
|
|
|
|
package iammiddleware
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/commerce/util/bit"
|
|
"github.com/hanzoai/commerce/util/permission"
|
|
)
|
|
|
|
// TestOrgAdminGrant_CoversOnboardingCatalog pins the self-serve onboarding
|
|
// contract: an org owner's grant satisfies the create gate of every catalog
|
|
// primitive step-1 onboarding needs — store (rest "store"/create =
|
|
// Admin|WriteStore), product, collection, variant — via the SAME intersection
|
|
// (bit.Field.Has) rest.CheckPermissions uses.
|
|
func TestOrgAdminGrant_CoversOnboardingCatalog(t *testing.T) {
|
|
need := []struct {
|
|
name string
|
|
mask bit.Mask
|
|
}{
|
|
{"WriteStore", permission.WriteStore},
|
|
{"ReadStore", permission.ReadStore},
|
|
{"Store(list)", permission.Store},
|
|
{"WriteProduct", permission.WriteProduct},
|
|
{"WriteCollection", permission.WriteCollection},
|
|
{"WriteVariant", permission.WriteVariant},
|
|
}
|
|
for _, n := range need {
|
|
if !orgAdminGrant.Has(n.mask) {
|
|
t.Errorf("orgAdminGrant lacks %s — an org owner cannot manage its own catalog", n.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOrgAdminGrant_ExcludesMoneyAuthority is the security invariant: the org
|
|
// grant must NEVER carry permission.Admin (the money/platform authority credit-
|
|
// mint + card-charge + cross-org billing gates key on). Widening it here would
|
|
// let an org owner mint balance or charge cards platform-wide.
|
|
func TestOrgAdminGrant_ExcludesMoneyAuthority(t *testing.T) {
|
|
if orgAdminGrant.Has(permission.Admin) {
|
|
t.Fatal("orgAdminGrant includes permission.Admin — org owners must not gain money/platform authority")
|
|
}
|
|
for _, m := range []struct {
|
|
name string
|
|
mask bit.Mask
|
|
}{
|
|
{"Secret", permission.Secret},
|
|
{"Authorize", permission.Authorize},
|
|
{"Capture", permission.Capture},
|
|
{"Payment", permission.Payment},
|
|
} {
|
|
if orgAdminGrant.Has(m.mask) {
|
|
t.Errorf("orgAdminGrant includes %s — org owners must not gain payment/secret authority", m.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIsOrgAdmin pins the trusted-header contract: the org-admin flag
|
|
// (X-User-IsOrgAdmin) or the SuperAdmin verdict (X-User-IsAdmin), exactly "true";
|
|
// absent or any other value fails closed.
|
|
func TestIsOrgAdmin(t *testing.T) {
|
|
cases := []struct {
|
|
val string
|
|
want bool
|
|
}{
|
|
{"true", true},
|
|
{"TRUE", false},
|
|
{"", false},
|
|
{"false", false},
|
|
{"1", false},
|
|
}
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
for _, tc := range cases {
|
|
c := app.TestCtx(http.MethodPost, "/v1/store")
|
|
if tc.val != "" {
|
|
c.Fiber().Request().Header.Set(HeaderUserIsAdmin, tc.val)
|
|
}
|
|
if got := isOrgAdmin(c); got != tc.want {
|
|
t.Errorf("isOrgAdmin(%q)=%v, want %v", tc.val, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The org-admin grant holds in the caller's home org only, and the home org is
|
|
// compared with the effective org verbatim: "Acme" is not "acme", and a padded name
|
|
// is no org at all.
|
|
func TestOrgAdminHomeMatches_IsVerbatim(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
for _, tc := range []struct {
|
|
home, eff string
|
|
want bool
|
|
}{
|
|
{"acme", "acme", true},
|
|
{"Acme", "acme", false},
|
|
{"acme", "ACME", false},
|
|
{" acme", "acme", false},
|
|
{"acme", "acme ", false},
|
|
{"", "", false},
|
|
{"acme", "", false},
|
|
} {
|
|
c := app.TestCtx(http.MethodPost, "/v1/store")
|
|
if tc.home != "" {
|
|
c.Fiber().Request().Header.Set(HeaderUserOwner, tc.home)
|
|
}
|
|
if got := orgAdminHomeMatches(c, tc.eff); got != tc.want {
|
|
t.Errorf("orgAdminHomeMatches(home %q, effective %q) = %v, want %v", tc.home, tc.eff, got, tc.want)
|
|
}
|
|
}
|
|
}
|