Skip to content

fix: requesting access does not name an account you could not list - #913

Merged
blaipr merged 1 commit into
mainfrom
fix/requesting-access-does-not-name-an-account-you-cannot-list
Sep 7, 2026
Merged

fix: requesting access does not name an account you could not list#913
blaipr merged 1 commit into
mainfrom
fix/requesting-access-does-not-name-an-account-you-cannot-list

Conversation

@blaipr

@blaipr blaipr commented Sep 7, 2026

Copy link
Copy Markdown
Member

?r=account/requestAccess/<id> rendered any account's name and client to any signed-in user,
for any id they cared to try.

Three things line up to make that true:

  • ACCOUNT_REQUEST is in the unconditional arm of Acl::checkUserAccess() — the same
    return true that serves the notification actions — so the action-level gate refuses nobody.
  • AccountRepository::getByIdEnriched() is a bare WHERE id = :id with no filter. That is
    deliberate: most of its callers pair it with an explicit per-account ACL check.
  • AccountRequestHelper::setViewForRequest() is the one helper in Helpers/Account/ that makes no
    such check — it tests $this->actionGranted (the action-level gate above) and then assigns the
    account straight to the template. AccountHelper::checkAccess() and
    AccountHistoryHelper::checkAccess() both do the per-account check.

Private accounts are reached too, which is the sharpest part: buildFilterPrivate() withholds them
from everybody, isAdminApp included, and this path went around it.

The companion saveRequest action reads the same way, so it is changed with it.

Why the obvious fix would have been wrong

The natural move is to copy AccountHelper::checkAccess(). That breaks the feature.

"Request Modification" exists to ask about an account you can see listed and cannot open:
AccountSearchItem::isShowRequest() is literally !$this->accountAcl->isShow(), and under global
search (AccountFilter::isFilterWithoutGlobalSearch()) a user with isAccGlobalSearch() lists
accounts they have no relationship with at all. Requiring view access would refuse exactly the case
the button is offered for.

So the bound is the search filter — the thing that decides what a user could have found — rather
than the ACL. getByIdEnrichedForUser() is getByIdEnriched() with AccountFilter::buildFilter()
applied, and an account the caller could not have listed is refused with the same "The account
doesn't exist" as one that is genuinely absent, so the two cannot be told apart.

The view is aliased to Account because that is the name the filter qualifies its conditions with;
account_data_v exposes every column they read — id, userId, userGroupId, isPrivate,
isPrivateGroup — confirmed against the running database rather than assumed.

Tests

requestAccessDoesNotNameAnAccountTheUserCannotList asserts the account's name is nowhere in the
response. The two reads are told apart by their statement, not their mapper, because both map to
AccountView and only the filtered one joins AccountToUser.

Mutation-verified in both directions. Worth recording that the first attempt passed against the
unfixed code for the wrong reason: a blanket query resolver answered every query with an
AccountView, so the request died on Invalid data's type. Expected: …ItemPreset before it ever
rendered, and "the name is absent" was true because nothing was. Scoping the resolver to the account
read — and letting everything else answer as the harness default does — makes the old code render
the page and fail the assertion, which is the proof that was wanted.

?r=account/requestAccess/<id> rendered any account's name and client to any signed-in
user. ACCOUNT_REQUEST sits in the unconditional arm of Acl::checkUserAccess() beside the
notification actions; getByIdEnriched() is a bare WHERE id = :id, unfiltered on purpose
because most callers pair it with an explicit ACL check; and AccountRequestHelper is the
one helper in its directory that makes no such check, where AccountHelper and
AccountHistoryHelper both do. Private accounts were reached too — buildFilterPrivate()
withholds those from everybody, isAdminApp included.

The obvious fix would break the feature: requesting a modification exists to ask about an
account you can see listed and cannot open — isShowRequest() is literally !isShow(), and
under global search that is an account you have no relationship with. So the bound is the
search filter, which decides listability, not the ACL. An account the caller could not
have listed is refused with the same 'doesn't exist' as one genuinely absent.

The view is aliased to Account because that is what the filter qualifies its conditions
with; account_data_v exposes every column they read, confirmed against the database.
@blaipr
blaipr merged commit 1be7150 into main Sep 7, 2026
8 checks passed
@blaipr
blaipr deleted the fix/requesting-access-does-not-name-an-account-you-cannot-list branch September 7, 2026 22:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant