Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/Application/Account/Services/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -520,11 +520,36 @@ public function restoreModified(AccountHistoryDto $dto): void
{
$this->accountRepository->transactionAware(
function () use ($dto) {
$userData = $this->context->getUserData();
$userProfile = $this->context->getUserProfile() ?? new ProfileData();

// Who owns the account, and which group it belongs to, are decided by the same two
// checks an ordinary edit makes — not by what the historical row happened to say.
//
// A restore writes every column of the snapshot back, and these two were among
// them, so restoring an old version silently reverted the owner and the group.
// Those are what AccountAcl compares the signed-in user against, so it changes who
// can see the account; and the door needs only ACCOUNT_EDIT_RESTORE, which
// AccountPermission buckets with plain edit — so anybody the account is shared
// with for editing could hand it back to a previous owner, which is exactly what
// userCanChangeOwner() exists to refuse them on the edit screen.
$changeOwner = false;
$changeUserGroup = false;

if (AccountAcl::getShowPermission($userData, $userProfile)) {
$account = $this->getById($dto->accountId);

$changeOwner = $this->userCanChangeOwner($userData, $userProfile, $account);
$changeUserGroup = $this->userCanChangeGroup($userData, $userProfile, $account);
}

$this->addHistory($dto->accountId);

$result = $this->accountRepository->restoreModified(
$dto->accountId,
AccountModel::restoreModified($dto, $this->context->getUserData()->id ?? 0)
AccountModel::restoreModified($dto, $userData->id ?? 0),
$changeOwner,
$changeUserGroup
);

if ($result->getAffectedNumRows() === 0) {
Expand Down
7 changes: 6 additions & 1 deletion src/Domain/Account/Ports/AccountRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ public function updatePassword(int $accountId, EncryptedPassword $encryptedPassw
* @throws ConstraintException
* @throws QueryException
*/
public function restoreModified(int $accountId, Account $account): QueryResult;
public function restoreModified(
int $accountId,
Account $account,
bool $changeOwner,
bool $changeUserGroup
): QueryResult;

/**
* Updates an item for bulk action
Expand Down
21 changes: 19 additions & 2 deletions src/Infrastructure/Adapter/Out/Account/Repositories/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,12 @@ public function updatePassword(int $accountId, EncryptedPassword $encryptedPassw
* @throws ConstraintException
* @throws QueryException
*/
public function restoreModified(int $accountId, AccountModel $account): QueryResult
{
public function restoreModified(
int $accountId,
AccountModel $account,
bool $changeOwner,
bool $changeUserGroup
): QueryResult {
$query = $this->queryFactory
->newUpdate()
->table(AccountModel::TABLE)
Expand All @@ -277,6 +281,11 @@ public function restoreModified(int $accountId, AccountModel $account): QueryRes
'countDecrypt',
'countView',
'dateEdit',
// Excluded here for the same reason update() excludes them: who owns an
// account and which group it belongs to is not an ordinary field. They are
// written back only when the caller is allowed to change them.
'userGroupId',
'userId',
'id',
]
)
Expand All @@ -285,6 +294,14 @@ public function restoreModified(int $accountId, AccountModel $account): QueryRes
->where('id = :id')
->bindValues(['id' => $accountId]);

if ($changeUserGroup) {
$query->col('userGroupId', $account->getUserGroupId());
}

if ($changeOwner) {
$query->col('userId', $account->getUserId());
}

$queryData = QueryData::build($query)->setOnErrorMessage(__u('Error on restoring the account'));

return $this->db->runQuery($queryData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,38 @@ static function (QueryData $arg) use ($id, $encryptedPassword) {
$this->account->updatePassword($id, $encryptedPassword);
}

/**
* A restore by somebody who may not change the owner does not change the owner.
*
* The snapshot carries every column, `userId` and `userGroupId` among them, and the restore
* wrote all of it back — so restoring an old version silently reverted who owns the account
* and which group it belongs to. Those are exactly what AccountAcl compares the signed-in user
* against, so it changes who can see it. And the door needs only ACCOUNT_EDIT_RESTORE, which
* AccountPermission buckets with plain edit, so anybody the account is shared with for editing
* could do it — the thing userCanChangeOwner() refuses them on the edit screen.
*
* @throws QueryException
* @throws ConstraintException
*/
public function testEditRestoreLeavesTheOwnerAloneWithoutThePermission(): void
{
$account = AccountDataGenerator::factory()->buildAccount();

$callback = new Callback(
static function (QueryData $arg) {
$params = $arg->getQuery()->getBindValues();

return !array_key_exists('userId', $params)
&& !array_key_exists('userGroupId', $params)
&& !empty($arg->getQuery()->getStatement());
}
);

$this->database->expects(self::once())->method('runQuery')->with($callback);

$this->account->restoreModified($account->getId(), $account, false, false);
}

/**
* @throws QueryException
* @throws ConstraintException
Expand Down Expand Up @@ -294,7 +326,7 @@ static function (QueryData $arg) use ($account) {

$this->database->expects(self::once())->method('runQuery')->with($callback);

$this->account->restoreModified($account->getId(), $account);
$this->account->restoreModified($account->getId(), $account, true, true);
}

/**
Expand Down