- Shipped
- September 23, 2026 at 12:42 PM UTC
- Author
- Kamo
- Commit
- 19505fa
Every get/update/delete-by-uid handler under CommerceMarketController's retail sub-resources (categories, brands, attributes/values, images, variants, tags, reviews, customers, customer tiers, tax classes/zones/rates, discounts, price lists/entries, gift cards, locations, stock levels/adjustments, shipping zones/methods, carriers, shipments, carts/cart items, draft orders, order notes) resolved the row through a bare findById(uid)/deleteById(uid), with no check that the row belonged to the caller's organization. Any authenticated member of any org could read, edit or delete another organization's rows by guessing or enumerating a uid - for customers (sequential Long ids) this was a PII leak of name, email and spend; for gift cards, stock levels and cart totals it was write access to another tenant's money and inventory figures. getMarketVendors had the same gap: its POST/PUT/DELETE siblings already resolved the market via findByIdAndOrganizationId, but the GET did not. Fixed by threading orgId into every one of these lookups: - Entities with their own organization column (most of them) now resolve through a new findByUidAndOrganizationId repository method, mirroring the existing **************** pattern (an explicit @Query, since these entities' id field is `uid`, not `id`). - Entities with no organization column of their own (product images/variants via their product, attribute values via their attribute, cart items via their cart, price list entries via their price list) are scoped through a new **************** query joining to the parent's org. - A handful of create-time foreign keys taken verbatim from the request body (a market id, a parent category id, a customer tier id, a tax zone/class, an image/variant cross-link, a shipment's carrier/location, a draft order's saved address) got the same org-scoped lookup, closing the same class of gap at write time, not just at the by-id read/update/delete. - lookupMarket and the new lookupOffering(id, orgId) overload centralise this for every create handler that previously called them unscoped. - getMarketVendors now resolves its market via findByIdAndOrganizationId before listing vendors, matching its own siblings. In every case a foreign-org row now answers exactly like a missing one: same exception, same message, same HTTP response the handler already produced for a bad uid - no new information is leaked by the fix itself. Mass assignment: updateGiftCard no longer accepts currentBalance from the request body. No existing RoleRightType (MANAGE_PRICING, MANAGE_ORDERS, ...) clearly covers manual balance adjustment, so per the coordinator's standing instruction we did not invent one; a same-org member can still edit the card's other fields exactly as before. Note for the coordinator: kamo-internal's MarketDiscountsTab.tsx gift-card edit dialog sends currentBalance today and that field will now be silently ignored - a dedicated adjust-balance endpoint (mirroring /stock-levels/{uid}/adjust) behind its own right is the right fix and needs a product decision, not a unilateral new RoleRightType. Also fixed in passing while re-deriving these signatures: createShipment read orderId from the wrong place (the controller was passing orgId positionally where orderId belonged; the UI always sends orderId in the POST body) - it now reads orderId from the body, which is what every caller already sends. This is a functional fix, not a security one. Tests: RetailServiceOrgScopingTest and **************** cover one representative resource per repository pattern (own org column, via-parent join) with a foreign-org read/update/delete that fails without the fix and a same-org call that succeeds, plus the gift-card mass- assignment rule. Mutation-checked: reverting the lookups to findById made all nine "foreign org" tests fail red; restored before commit.
