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.
115 lines
6.0 KiB
Go
115 lines
6.0 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 (
|
|
"errors"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/commerce/middleware/iammiddleware"
|
|
"github.com/hanzoai/commerce/mintauth"
|
|
"github.com/hanzoai/commerce/util/json/http"
|
|
)
|
|
|
|
// AuthorizeMint stamps this request's datastore context as authorized to mint
|
|
// spendable balance, so a downstream ledger write (through an
|
|
// Organization.Namespaced-derived datastore) passes mintauth.Enforce. It is the
|
|
// ONE way an HTTP handler grants the mint capability the ledger sink demands —
|
|
// call it AFTER establishing a proven mint authority (MayMintMoney, a settled
|
|
// payment, or a server-fixed grant). It re-stores the "context" key, which
|
|
// Organization.Namespaced reads, so any datastore the handler builds afterward
|
|
// is authorized. Writes that build their datastore BEFORE the authority is known
|
|
// (top-up after the charge, webhook after signature) instead authorize the
|
|
// specific write via mintauth.WithAuthorized on that write's context.
|
|
func AuthorizeMint(c *zip.Ctx) {
|
|
// SetContext, not a locals key: c.Context() IS the request context under
|
|
// the one-context model, so the mint authorization must land THERE for
|
|
// the ledger sink (and every datastore built from c.Context()) to see it.
|
|
// SetContext, not a locals key: c.Context() IS the request context under
|
|
// the one-context model, so the mint authorization must land THERE for
|
|
// the ledger sink (and every datastore built from c.Context()) to see it.
|
|
c.SetContext(mintauth.WithAuthorized(c.Context()))
|
|
}
|
|
|
|
// PlatformOnly restricts a route to the ONE principal allowed to MINT money /
|
|
// spendable balance: a Hanzo PLATFORM SuperAdmin — auth.IAMClaims.IsSuperAdmin(),
|
|
// the identity edge's authz.Claims.Sudo verdict (X-User-IsAdmin): a PERSON whose
|
|
// own org is the reserved "admin" org. No machine is one. An application IAM
|
|
// minted under the admin org, and an admin-org person's API key, both carry the
|
|
// org and used to be admitted on it; neither is SuperAdmin anywhere else, and
|
|
// neither is here.
|
|
//
|
|
// There used to be a second one — a bearer equal to COMMERCE_SERVICE_TOKEN,
|
|
// verified here. That was commerce doing IAM's job with a secret of its own.
|
|
//
|
|
// It deliberately does NOT admit the org-level Admin bit (permission.Admin). An
|
|
// org OWNER carries org-level IsAdmin=true within their own org (IAM), which the
|
|
// gateway/EdgeAuth mints into X-User-Permissions = Admin|Live; a legacy per-org
|
|
// access token can hold Admin too. Gating the money-mint billing routes on that
|
|
// bit — via TokenRequired(permission.Admin) alone — let ANY org owner self-credit
|
|
// unlimited balance (POST /v1/billing/deposit &c.) → unlimited free inference.
|
|
// That is the real-money-GA blocker this gate closes. It is the same
|
|
// org-admin-vs-SuperAdmin anti-conflation the codebase enforces for cross-org
|
|
// actions (checkout tenant admin, the edge billing ?org override).
|
|
//
|
|
// MOUNT IT AFTER TokenRequired(permission.Admin): TokenRequired resolves the org
|
|
// and sets c["permissions"]; PlatformOnly then NARROWS who may proceed to the handler.
|
|
// It never widens access — a caller already rejected by TokenRequired (401) never
|
|
// reaches here.
|
|
//
|
|
// Fail-closed: neither signal present → 403, handler not reached.
|
|
func PlatformOnly() zip.Handler {
|
|
// The fiber-handler shape: continue by advancing fiber's own chain.
|
|
return func(c *zip.Ctx) error { return PlatformOnlyMW(func(c *zip.Ctx) error { return c.Next() })(c) }
|
|
}
|
|
|
|
// PlatformOnlyMW is the SAME decision in middleware shape, for zip's typed-op
|
|
// path (Router.OpScope). An op's continuation is the `next` it is handed, NOT
|
|
// fiber's chain — the op's handler is wrapped inside this middleware rather than
|
|
// chained after it. A gate that continues with c.Next() therefore finds nothing
|
|
// left to run and yields 404, which the caller then reports as an error, so an
|
|
// AUTHORIZED request to a typed mint op never reaches its handler at all.
|
|
// Proven by TestMintOpScope_AuthorizedOpRunsHandlerOnce.
|
|
//
|
|
// One decision, two shapes — deliberately not two copies. This is a money-mint
|
|
// gate; a second implementation is a second thing to get wrong, and the failure
|
|
// is silent authorization rather than a compile error.
|
|
func PlatformOnlyMW(next zip.Handler) zip.Handler {
|
|
return func(c *zip.Ctx) error {
|
|
if MayMintMoney(c) {
|
|
// Proven mint principal → authorize the ledger sink for this request,
|
|
// so the PlatformOnly-gated mint handlers (deposit,
|
|
// credit-grants, payouts, cycle, auto-recharge, …) all mint without
|
|
// per-handler changes while org-admins are still 403'd above.
|
|
AuthorizeMint(c)
|
|
return next(c)
|
|
}
|
|
return http.Fail(c, 403,
|
|
"This operation requires platform-administrator credentials.",
|
|
errors.New("money-mint route: caller is not a platform admin"))
|
|
}
|
|
}
|
|
|
|
// IsPlatform is THE definition of the PLATFORM PRINCIPAL — a caller acting for
|
|
// the platform itself rather than for a tenant: a Hanzo PLATFORM SuperAdmin,
|
|
// auth.IAMClaims.IsSuperAdmin(), the edge's verdict. The platform's own scheduled
|
|
// work runs in process (plane ops, the in-process transport, which states the
|
|
// platform principal itself), so it needs no door of its own.
|
|
//
|
|
// It deliberately does NOT admit the org-level Admin bit (an org OWNER's IAM
|
|
// isAdmin, or a legacy per-org access token), nor any machine.
|
|
func IsPlatform(c *zip.Ctx) bool {
|
|
return iammiddleware.GetIAMClaims(c).IsSuperAdmin()
|
|
}
|
|
|
|
// MayMintMoney is THE single predicate for "may this caller MINT money /
|
|
// spendable balance": the platform principal, and only it. Use it wherever a
|
|
// mint decision is made OUTSIDE the route-middleware chain — the ZAP-over-HTTP
|
|
// dispatcher gates its money-mint method (billing.deposit) with it, and the
|
|
// allotment grant clamps a client plan override on it — so there is ONE
|
|
// expression of the mint principal, shared by the route gate (PlatformOnly) and
|
|
// every in-handler gate.
|
|
func MayMintMoney(c *zip.Ctx) bool { return IsPlatform(c) }
|