fix: hold an account to the password lifetime policy when it is edited - #828
Merged
Merged
Conversation
A password preset marked fixed sets a maximum lifetime, and
`checkPasswordPreset()` clamps `passDateChange` to `time() + expireTime`. It is
called where a password is being set — creating an account, copying one,
changing its password, on both the web and the API — and nowhere else.
Editing an account writes `passDateChange` just the same, from a field the form
offers (`password_date_expire`), and none of those paths clamped it: an account
created under a ninety-day policy could be edited a moment later to expire in a
decade, and bulk edit could do it to a whole selection at once. The API's
`account/edit` takes the same value as `expireDate`.
The clamp is now its own method and the edit paths call that, not the whole
check. `checkPasswordPreset()` also validates the password against the preset,
and an edit legitimately carries none — `PasswordValidator::validate()` measures
`mb_strlen('')` against the required length and throws, so calling the whole
thing there would have refused every edit while a fixed preset existed. That is
what `expects(never())->method('validate')` in the new test is holding.
`checkPasswordPreset()` delegates to the same clamp, so the paths that already
enforced it are unchanged — its existing tests pass untouched.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A password preset marked fixed sets a maximum lifetime, and
checkPasswordPreset()clampspassDateChangetotime() + expireTime. It is called where a password is being set — create, copy, edit-pass, on both the web and the API — and nowhere else.Editing an account writes
passDateChangejust the same, from a field the form offers (password_date_expireinaccount.incandaccount_bulkedit.inc;expireDateon the API). None of those paths clamped it:ACCOUNT_EDIT— create the account under a ninety-day policy, edit it a moment later to expire in a decade;ACCOUNTMGR_BULK_EDIT— the same, to a whole selection at once;account/edit— the same, asexpireDate.Anyone with edit rights on an account could exempt it from a rotation policy the administrator had made mandatory.
Why not just call the existing check
Because it would refuse every edit.
checkPasswordPreset()also runsPasswordValidator::validate(), which measuresmb_strlen($password)against the preset's required length and throws — and an edit legitimately carries no password. While a fixed preset existed, editing any account would have failed.So the clamp becomes its own method,
checkPasswordExpiry(), and the edit paths call that.checkPasswordPreset()delegates to the same clamp, so the paths that already enforced it are untouched — its twelve existing tests pass unchanged.expects(never())->method('validate')in the new test is holding that distinction, and it is as much the point as the clamp:Three things the full suite caught
All mine, all from the new call site: two PHPStan complaints (
@throwsnaming classes in the wrong namespace, and the widenedAccountDtoreachingAccountService::update()which wantsAccountUpdateDto— fixed with@template T of AccountDto), andAccountFormTest, whose mocked preset service returned a stub for the new call and broke the form's typed property.3991 unit + 976 integration pass; PHPStan level 6 on
srcand PHPCS clean.