Repository navigation
fix(auth): bind MaxPurchaseAmount to total commitment, enforce on retry (follow-up to #1210) - #1477
Conversation
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 16 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughPurchase validation now rejects negative recurring costs, constraint calculations include full commitment, and both fresh execution and retry flows enforce purchase constraints before persistence. ChangesPurchase constraint enforcement
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant retryPurchase
participant enforcePurchaseConstraints
participant HasPermissionForConstraintsAPI
participant SavePurchaseExecution
retryPurchase->>enforcePurchaseConstraints: failed execution recommendations
enforcePurchaseConstraints->>HasPermissionForConstraintsAPI: total commitment constraints
HasPermissionForConstraintsAPI-->>enforcePurchaseConstraints: allow or deny
enforcePurchaseConstraints-->>retryPurchase: constraint result
retryPurchase->>SavePurchaseExecution: persist successor execution
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
9a62a6b to
e7f9585
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
purchaseConstraintSets capped MaxPurchaseAmount against a batch's total upfront cost only. A no-upfront or partial-upfront commitment's real cost is the recurring monthly charge over the term, not the (possibly zero) upfront alone, so an honest no-upfront purchase could evade an otherwise-binding cap. Fix the basis to the batch's total commitment (upfront plus monthly_cost * term_months, recTotalCommitment) and reject a batch whose computed total is exactly zero rather than silently reading MaxPurchaseAmount==0 as "unconstrained" (requireNonZeroCommitment). Also reject a negative monthly_cost at the request boundary, since it would otherwise offset the recurring leg and reopen the same evasion. retryPurchase never consulted the execute:purchases permission Constraints at all, so a retry-any/retry-own session could replay another user's over-cap failed purchase, and a permission tightened after the original submission never applied to a replay. Route both the direct-execute and retry paths through a new shared enforcePurchaseConstraints helper so they check identical Constraints. Adversarial review follow-up to #1210 (SEC-01, issue #1141).
e7f9585 to
5355179
Compare
Summary
Adversarial code-review follow-up to #1210 (SEC-01, issue #1141). Two live evasion bugs against the
MaxPurchaseAmountpermission constraint, confirmed against currentmain:Basis was upfront-cost-only, not total commitment.
purchaseConstraintSetssummed onlyrec.UpfrontCostacross the batch. A no-upfront (or partial-upfront) RI/Savings Plan hasUpfrontCostat or near zero, so the constraint always passed regardless of the real committed spend (monthly_cost * term). A user capped at e.g. $1,000 could submit an arbitrarily large no-upfront purchase through the ordinary UI, with entirely honest data, and evade the cap.Fix:
purchaseConstraintSetsnow sums each rec's total commitment via a newrecTotalCommitmenthelper:UpfrontCost + MonthlyCost * (Term * 12).Termis years (AWS/Azure/GCP standard, matches the*12convention already used inexchange_lookup.go);UpfrontCost/MonthlyCostare both batch totals for the rec'sCount(not per-instance), so they sum directly with no rescaling.Also added:
requireNonZeroCommitment: a batch whose computed total commitment is exactly$0is now rejected (400) rather than silently read as "unconstrained" (MaxPurchaseAmount==0means uncapped to the matcher) -- per this repo's no-silent-fallback-on-money-paths convention.validatePurchaseRecommendationguard rejecting a negativemonthly_cost, since the new formula would otherwise let a negative value offset/mask a real upfront cost and reopen the same evasion.retryPurchasebypassed the constraint gate entirely.requirePermissionConstraintshad exactly two call sites onmain(executePurchase, ri-exchange execute); the retry path never called it. A retry-any/retry-own session could relaunch a previously-failed purchase whose recommendations exceed their ownexecute:purchasesConstraints, and a permission tightened after the original submission never applied to a replay.Fix: extracted a shared
enforcePurchaseConstraints(ctx, session, recs)helper (builds the constraint sets, runs the zero-commitment guard, thenrequirePermissionConstraints) and call it from bothvalidateExecutePurchaseRequestandretryPurchase, so both paths check identical Constraints.Known follow-up (documented, not fixed here)
The constraint check still compares against client-supplied
upfront_cost/monthly_costin the request JSON; it does not re-price or re-quote against the provider at execution time. A fully malicious client (as opposed to the honest-UI-data scenario this PR closes) could still fabricate a near-zeromonthly_costto approximate the old evasion. Fully closing that requires binding the checked amount to a server-side quote before purchase, which touches the AWS/Azure/GCP purchase client code broadly -- out of scope for this focused fix per the reviewing team's guidance. Left as a documented gap; happy to file a tracking issue if wanted.Testing
go build ./...-- exit 0go vet ./...-- exit 0go test ./internal/api/... ./internal/auth/...-- exit 0 (2461 tests)golangci-lintv2.10.1 (CI-pinned, matchesci.yml) oninternal/api-- 0 issuesgocyclo -over 10 -ignore "_test\.go"on touched files -- exit 0 (clean)go test ./...(full repo): did not complete in this environment due to resource contention with a concurrent build in a sibling worktree (process repeatedly died mid-run across 3 attempts, no failure output, just never reached completion). Not run to green; flagging rather than claiming it passed.Regression tests
TestHandler_executePurchase_NoUpfrontBatch_TotalCommitmentEnforced: a no-upfront batch ($0 upfront, $600/mo, 3yr term = $21,600 real commitment). Verified to fail pre-fix (constraint set carriedMaxPurchaseAmount=0, mock expectation for21600.0never matched) and pass post-fix.TestHandler_retryPurchase_PermissionConstraintsDenied: retry of a failed batch whose total commitment ($5,000) exceeds the retrying session's cap. Verified to fail pre-fix (panicked on an unexpectedSavePurchaseExecutioncall -- no constraint check ever ran) and pass post-fix (403, no execution persisted).Closes/references #1210.
Summary by CodeRabbit
execute:purchasespermission constraints for the retrying session before saving.