mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
A card subscription with no userId lands on the org account, so a plain member could buy the org a plan, and in an ecosystem org without a card. The org account now needs a privileged billing caller or an admin of the org the request acts in (X-User-IsOrgAdmin, minted for the effective org); a member subscribes their own account. Co-authored-by: Hanzo Dev <[email protected]>
79 lines
3.3 KiB
Go
79 lines
3.3 KiB
Go
package billing
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/commerce/middleware/iammiddleware"
|
|
"github.com/hanzoai/commerce/util/bit"
|
|
"github.com/hanzoai/commerce/util/permission"
|
|
)
|
|
|
|
// isPrivilegedBillingCaller reports whether the authenticated caller may act on
|
|
// billing records belonging to ANY subject within the resolved org — the
|
|
// service-to-service and administrative callers that legitimately operate on a
|
|
// user's behalf (cloud-api, the auto-recharge cron, an org owner, a platform
|
|
// admin). Any of three independent signals suffices:
|
|
//
|
|
// - the Admin permission bit — set by middleware.TokenRequired for a verified
|
|
// COMMERCE_SERVICE_TOKEN and for a legacy org admin access token, and by
|
|
// iammiddleware.IAMTokenRequired for a gateway/EdgeAuth-minted org admin
|
|
// (X-User-Permissions). This is the ONLY signal a stripped-identity service
|
|
// token carries (EdgeAuth deletes its X-* headers), so it must be checked.
|
|
// - the org-level IsAdmin claim (an org owner acting inside their own org); or
|
|
// - a platform global admin (IsSuperAdmin() — owner=="admin" — the
|
|
// "admin" org), who may retarget any org via EdgeAuth's ?org override.
|
|
//
|
|
// It reads c["permissions"] defensively (no MustGet) so a handler reached
|
|
// without a permission-setting middleware fails CLOSED (treated as
|
|
// non-privileged) rather than panicking.
|
|
func isPrivilegedBillingCaller(c *zip.Ctx) bool {
|
|
if v := c.Locals("permissions"); v != nil {
|
|
if f, ok := v.(bit.Field); ok && f.Has(permission.Admin) {
|
|
return true
|
|
}
|
|
}
|
|
claims := iammiddleware.GetIAMClaims(c)
|
|
return claims.IsAdmin || claims.IsSuperAdmin()
|
|
}
|
|
|
|
// callerMayReachBillingSubject reports whether the authenticated caller is
|
|
// authorized to act on a per-user billing record whose owner is one of
|
|
// ownerIDs (its CustomerId / UserId — the fields that name whose money/instrument
|
|
// the record is). It is the intra-org companion to EdgeAuth's namespace +
|
|
// billing-subject lock: EdgeAuth already pins the namespace and the pinnable body
|
|
// keys (user/userId/customerId) to the caller's org, but a URL :id path-param
|
|
// (GetById handlers) and the paymentMethodId body field — neither of which
|
|
// EdgeAuth pins — can still NAME a different subject's record inside that same
|
|
// org. Cross-ORG is prevented by the namespace; this closes the residual
|
|
// INTRA-org gap.
|
|
//
|
|
// Privileged callers always pass. Everyone else passes only when one of ownerIDs
|
|
// equals their own per-org billing subject — the org slug returned by
|
|
// orgBillingKey, which is exactly the key EdgeAuth pins those records under
|
|
// (== claims.Owner == org.Name == namespace). Fail-closed: an empty subject, or
|
|
// no owner matching it, denies.
|
|
func callerMayReachBillingSubject(c *zip.Ctx, ownerIDs ...string) bool {
|
|
if isPrivilegedBillingCaller(c) {
|
|
return true
|
|
}
|
|
subject := orgBillingKey(c)
|
|
if subject == "" {
|
|
return false
|
|
}
|
|
for _, o := range ownerIDs {
|
|
if strings.EqualFold(strings.TrimSpace(o), subject) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// mayActForOrg reports whether the caller may act for the org's own account: a
|
|
// privileged billing caller (service, platform SuperAdmin) or an admin of the org
|
|
// the request acts in.
|
|
func mayActForOrg(c *zip.Ctx) bool {
|
|
return isPrivilegedBillingCaller(c) || iammiddleware.ActsAsOrgAdmin(c)
|
|
}
|