You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Scoped down 2026-08-08. The security-critical core landed in PR #1744: grantAdmin no longer stubs the authorization decision, and the #923 carve-out now has handler-level coverage it previously lacked entirely. This issue now tracks only the remainder. Full status, corrected counts and the mutation evidence are in this comment.
Remaining work
1. Per-handler expected-pair pinning
grantAdmin now answers each (action, resource) correctly through the real matcher, which fully closes the carve-out class. It does not catch a handler asking the wrong non-carved verb — view:purchases where it should ask approve:purchases still passes, because a real admin holds both. Each handler needs to declare the pairs it is contractually allowed to ask, with anything else failing. 235.grantAdmin() invocations across 26 files.
2. Per-handler decision on whether the permission check is unconditional
The original bullet ("drop .Maybe() wherever the permission check is mandatory") cannot be done as written, and that is a finding rather than a task nobody got to.
It presumes "mandatory" is a property of the check. It is not — it is a per-handler contract, and for many handlers the check is legitimately never reached. requirePermission runs after input validation in several handlers: updateGroup, getGroup and deleteGroup all call validateUUID(groupID) first, and createGroup/updateGroup return 400 invalid request body on an unmarshal failure. Every test covering those paths would fail AssertExpectations under a blanket .Maybe() removal, and none of those tests is wrong — the handler correctly never asked.
So the work is to decide per handler whether the check is unconditional, and assert the call explicitly where it is. .Maybe() is retained in grantPermissionsScoped with a comment recording this, so the reasoning is not re-derived from scratch.
3. Restricted-account coverage per handler
grantScoped(accounts...) exists and the shared seam (getAllowedAccounts -> requireAccountAccess) is covered by four tests. What remains is exercising the restricted path per handler across the 17getAllowedAccounts call sites in 9 files: handler_accounts.go (2), handler_analytics.go (1), handler_dashboard.go (2), handler_history.go (1), handler_ladder.go (1), handler_marketplace.go (1), handler_recommendations.go (1), handler_ri_exchange.go (4), scoping.go (4) — plus one direct GetAllowedAccountsAPI call in handler_purchases_revoke.go.
Each of the three is independently landable and independently reviewable, which the original 235-site sweep would not have been.
Original issue text (counts below are as-reviewed at be11bdcb5; see the status comment for corrections)
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.
So every (userID, action, resource) triple returns true, and the allowed-account list is always nil, which the API layer reads as unrestricted. .Maybe() additionally means AssertExpectations can never require that the permission check happened at all.
The authorization decision is the thing under test in a large fraction of these 227 tests, and it is stubbed to a constant.
Failure scenario
Two distinct regression classes are invisible:
Wrong or absent check. A handler that asks for the wrong verb/resource pair - say view/purchases where it should ask approve/purchases - is indistinguishable from a correct one, because the wildcard matcher returns true for both. A handler that skips the permission check entirely is also indistinguishable, because .Maybe() means the unconsumed expectation is never reported.
This is worth stating plainly, because it explains a pattern rather than just naming a defect: the 2026-07-28 review found several account-scoping holes in the API layer that the test suite did not, and this is why. The suite is structurally unable to see them. Any handler-level scoping fix validated only by grantAdmin tests has not been validated.
Why this is a prerequisite, not a cleanup task
Together with the AssertNotCalled vacuity filed alongside it, this determines whether "fixed, tests are green" means anything on the API authorization surface. It is not exploitable on its own - no production code path is wrong because of it - but it invalidates the safety net that every other authorization fix depends on. Treat it as a precondition for accepting the next "fixed with green tests" claim on a scoping or permission bug.
Fix direction
Make grantAdmin assert the specific (action, resource) pairs a handler is contractually allowed to ask for, and fail on any other pair, so asking the wrong question is a test failure rather than a pass.
Drop .Maybe() wherever the permission check is mandatory, so a handler that stops checking fails AssertExpectations.
Add a companion grantScoped(accounts...) helper returning a non-nil allow-list, and convert the account-scoped handler tests to use it, so the restricted path is exercised at least once per handler that claims to honour it.
Expect the conversion to turn up real failures. Each one is a scoping behaviour that has never been tested.
The AssertNotCalled / isExpected critical filed alongside this one: same theme, different mock, and the two together account for most of the API layer's assertion surface.
Remaining work
1. Per-handler expected-pair pinning
grantAdminnow answers each(action, resource)correctly through the real matcher, which fully closes the carve-out class. It does not catch a handler asking the wrong non-carved verb —view:purchaseswhere it should askapprove:purchasesstill passes, because a real admin holds both. Each handler needs to declare the pairs it is contractually allowed to ask, with anything else failing. 235.grantAdmin()invocations across 26 files.2. Per-handler decision on whether the permission check is unconditional
The original bullet ("drop
.Maybe()wherever the permission check is mandatory") cannot be done as written, and that is a finding rather than a task nobody got to.It presumes "mandatory" is a property of the check. It is not — it is a per-handler contract, and for many handlers the check is legitimately never reached.
requirePermissionruns after input validation in several handlers:updateGroup,getGroupanddeleteGroupall callvalidateUUID(groupID)first, andcreateGroup/updateGroupreturn400 invalid request bodyon an unmarshal failure. Every test covering those paths would failAssertExpectationsunder a blanket.Maybe()removal, and none of those tests is wrong — the handler correctly never asked.So the work is to decide per handler whether the check is unconditional, and assert the call explicitly where it is.
.Maybe()is retained ingrantPermissionsScopedwith a comment recording this, so the reasoning is not re-derived from scratch.3. Restricted-account coverage per handler
grantScoped(accounts...)exists and the shared seam (getAllowedAccounts->requireAccountAccess) is covered by four tests. What remains is exercising the restricted path per handler across the 17getAllowedAccountscall sites in 9 files:handler_accounts.go(2),handler_analytics.go(1),handler_dashboard.go(2),handler_history.go(1),handler_ladder.go(1),handler_marketplace.go(1),handler_recommendations.go(1),handler_ri_exchange.go(4),scoping.go(4) — plus one directGetAllowedAccountsAPIcall inhandler_purchases_revoke.go.Each of the three is independently landable and independently reviewable, which the original 235-site sweep would not have been.
Original issue text (counts below are as-reviewed at
be11bdcb5; see the status comment for corrections)Reviewed commit:
be11bdcb5. Note:origin/mainmoved to3e9660d06during the review; re-verify against currentmainbefore changing code, since a finding may have been fixed or moved.Where
internal/api/mocks_test.go:285-297-grantAdmin()internal/api/handler_purchases_test.go(45),handler_plans_test.go(33),handler_config_test.go(26),handler_coverage_test.go(24),router_handlers_test.go(24),handler_test.go(19),handler_apikeys_test.go(14)What
grantAdmin()registers:On("HasPermissionAPI", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(true, nil).Maybe()HasPermissionForConstraintsAPIOn("GetAllowedAccountsAPI", ...).Return([]string(nil), nil).Maybe()So every
(userID, action, resource)triple returnstrue, and the allowed-account list is alwaysnil, which the API layer reads as unrestricted..Maybe()additionally meansAssertExpectationscan never require that the permission check happened at all.The authorization decision is the thing under test in a large fraction of these 227 tests, and it is stubbed to a constant.
Failure scenario
Two distinct regression classes are invisible:
Wrong or absent check. A handler that asks for the wrong verb/resource pair - say
view/purchaseswhere it should askapprove/purchases- is indistinguishable from a correct one, because the wildcard matcher returnstruefor both. A handler that skips the permission check entirely is also indistinguishable, because.Maybe()means the unconsumed expectation is never reported.Account scoping. With
GetAllowedAccountsAPIpinned tonil, nograntAdmintest can ever exercise a restricted account list. That is the exact class of bug (sec(purchases): standard user can pause/resume other users' scheduled purchases (no ownership gate) #950 / fix(api): filter purchases by account UUID AND external id (closes #701, #498, #866) #956) that survived four successive "fixed with green tests" rounds: each fix merged with a green suite and a clean review while the account filter stayed broken in production, because the tests filtered a column the data did not use and ran against a mock that had no restriction to enforce.This is worth stating plainly, because it explains a pattern rather than just naming a defect: the 2026-07-28 review found several account-scoping holes in the API layer that the test suite did not, and this is why. The suite is structurally unable to see them. Any handler-level scoping fix validated only by
grantAdmintests has not been validated.Why this is a prerequisite, not a cleanup task
Together with the
AssertNotCalledvacuity filed alongside it, this determines whether "fixed, tests are green" means anything on the API authorization surface. It is not exploitable on its own - no production code path is wrong because of it - but it invalidates the safety net that every other authorization fix depends on. Treat it as a precondition for accepting the next "fixed with green tests" claim on a scoping or permission bug.Fix direction
grantAdminassert the specific(action, resource)pairs a handler is contractually allowed to ask for, and fail on any other pair, so asking the wrong question is a test failure rather than a pass..Maybe()wherever the permission check is mandatory, so a handler that stops checking failsAssertExpectations.grantScoped(accounts...)helper returning a non-nil allow-list, and convert the account-scoped handler tests to use it, so the restricted path is exercised at least once per handler that claims to honour it.Expect the conversion to turn up real failures. Each one is a scoping behaviour that has never been tested.
Related
sec(plans): listPlans does not scope account_ids to caller's allowed_accounts), bug(api/dashboard): calculateCommitmentMetrics does not intersect allowed_accounts for scoped sessions #959 (calculateCommitmentMetrics does not intersect allowed_accounts), sec(api): listExchangeableAzureRIs returns tenant-wide reservations without allowed-accounts scoping #1532, Azure RI exchange: source reservations are never scoped to the authorized subscription #1527 - live scoping issues in exactly the surface these 227 tests nominally cover.AssertNotCalled/isExpectedcritical filed alongside this one: same theme, different mock, and the two together account for most of the API layer's assertion surface.