KamoCRM

Org-scope RetailService and ****************

Fixkamo-shared-library
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.

All changes

Like what you see shipping?

All of it arrives in your workspace on its own. Start on the free plan and read this page again in a month.

Start Free ForeverView Pricing