Skip to content

feat(security): reject pending and blocked users at login (#63)#88

Open
martinydeAI wants to merge 3 commits into
feature/issue-45-user-entity-extensionfrom
feature/issue-63-account-status-checker
Open

feat(security): reject pending and blocked users at login (#63)#88
martinydeAI wants to merge 3 commits into
feature/issue-45-user-entity-extensionfrom
feature/issue-63-account-status-checker

Conversation

@martinydeAI

@martinydeAI martinydeAI commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Links to issues

Closes #63.

do-not-merge — depends on PR #86 (#45 + #83).

This branch is stacked on feature/issue-45-user-entity-extension
because the checker reads User::$status, which doesn't exist on
develop yet. Once PR #86 lands, this PR is rebased onto
develop and the do-not-merge label is removed.

Description

Adds App\Security\AccountStatusChecker implementing Symfony
Security's UserCheckerInterface. The pre-auth hook rejects any
User whose status is not Approved:

  • PendingCustomUserMessageAccountStatusException("account.pending")
  • BlockedCustomUserMessageAccountStatusException("account.blocked")
  • Approved → no-op (sign-in proceeds normally)

Wired on the main firewall via security.yaml's user_checker:
key. Translations live in a new translations/security.da.yaml
(the security domain that the login template already renders
error messages from via error.messageKey|trans({}, 'security')).

Identity state (this PR) stays orthogonal to authorisation
(PR #87's voter) per ADR 006 — a Blocked user retains the roles
they had before being blocked, they just can't sign in to exercise
them.

Screenshot of the result

n/a — pre-auth hook, no new UI surface. The login form renders the
new localised error inside the existing error block.

Checklist

  • My code is covered by test cases.
  • My code passes our test (all our tests).
  • My code passes our static analysis suite.
  • My code passes our continuous integration process.

Verified locally:

  • task coding-standards-check — all green.
  • task test-coverage — 58 tests, 203 assertions, 100 %
    coverage
    .

Additional comments or questions

UserCheckerInterface::checkPreAuth() and checkPostAuth() in
Symfony 8 carry an optional ?TokenInterface $token = null second
argument; the override matches that signature. The token slot is
unused here — neither identity check needs it.

The login template (templates/security/login.html.twig)
already renders error.messageKey|trans(error.messageData, 'security')
in its error block, so the new translations slot in without any
template change. That's why the keys had to land in the security
translation domain rather than the project's messages domain.

The two new SecurityControllerTest methods create their pending /
blocked users inside the test via UserManager::createUser(..., status: UserStatus::Pending|Blocked) rather than seeding via
UserFixtures. DAMA's per-test transaction rolls them back so the
baseline Alice + Bob stay untouched.


Details - AI specificities

  • ADR: docs/adr/006-user-approval-and-account-state.md
    (Draft; lands via docs: add ADR 004 — user registration, approval, and account state #61). Specifically the "Login gating"
    section.
  • Stacked on PR feat(user): add name and status fields per ADR 006 (#45, #83) #86. Once that PR merges:
    1. Rebase this branch onto fresh develop.
    2. Remove the do-not-merge label.
    3. Update the body to drop the block reason.
  • Out of scope:
  • Test changes pre-approved: the two new methods on
    SecurityControllerTest are additive (no existing assertions
    changed); same approval as PR 1.
  • Translation file is new. Symfony auto-loads
    translations/*.<locale>.yaml; no cache clear needed in tests
    (the kernel boots fresh per integration suite via
    tests/bootstrap_integration.php).

Adds `App\Security\AccountStatusChecker` implementing Symfony
Security's `UserCheckerInterface`. The pre-auth hook rejects any
`User` whose `status` is not `Approved`, throwing a
`CustomUserMessageAccountStatusException` with a localised key:
`account.pending` for users still awaiting approval and
`account.blocked` for users an admin has revoked. Translations land
in a new `translations/security.da.yaml` (the `security` domain the
login template already renders error messages from).

Wired on the `main` firewall via `security.yaml`'s `user_checker:`
key. Identity state stays orthogonal to authorisation per ADR 006 —
the voter from PR #87 reads roles + email, the checker reads
status; neither overlaps with the other.

Sequenced as PR 3 of the User management milestone plan. Stacked
on PR 1 (#86) since it reads `User::$status` which doesn't exist on
`develop` yet — labelled `do-not-merge` until #86 lands.

Closes #63.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinydeAI martinydeAI added the do-not-merge Block merging until external dependency lands label Jun 19, 2026
Clarify comment about 'Blocked' user roles in AccountStatusChecker.
* {@see CustomUserMessageAccountStatusException} halts the flow and
* surfaces the (localised) translation key on the login form.
*
* A `Blocked` user retains the roles

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we strip roles from blocked users for good measure, or is that bad practice?

Comment thread tests/Integration/Controller/SecurityControllerTest.php
Comment thread tests/Unit/Security/AccountStatusCheckerTest.php
Per review on PR #88 - apply the test-comment convention to the
new AccountStatusCheckerTest and the two new login-rejection
methods in SecurityControllerTest. Each `test...` method now
opens with a single-line "Tests ...", "Ensures ...", or
"Verifies ..." comment naming what it asserts.

Pure documentation change - no test logic touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinyde martinyde requested a review from tuj June 19, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Block merging until external dependency lands

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants