Password Policy — Improvement & Growth Plan
Package: capell-app/password-policy · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Password Policy adds opt-in admin password controls to Capell: password expiry, forced “must change”, recent-password reuse blocking, and Laravel’s compromised-password (HIBP) check. It surfaces through a package settings page backed by PasswordPolicySettingsSchema, an Extensions-page settings surface for package management, and a forced-change page (ForcedPasswordChangePage) gated by an auth middleware (EnsurePasswordPolicyCompliance) — plus columns/filters/row+bulk actions injected into the core Users table via PasswordPolicyUserTableExtender / PasswordPolicyUserFormExtender and console commands for expiry, forced-change marking, history pruning, and diagnostics. Domain logic lives in Actions (ValidatePasswordChangeAction, EvaluatePasswordPolicyAction, UpdatePasswordAction, RecordPasswordHistoryAction, MarkUserForPasswordChangeAction, PrunePasswordHistoryAction, BuildPasswordSecurityPostureReportAction); persistence is two users columns (password_changed_at, must_change_password), the password_policy_password_histories table, and a password_policy settings group. Deps: capell-app/admin, capell-app/core, lorisleiva/laravel-actions, spatie/laravel-data, spatie/laravel-package-tools.
Current marketplace summary: “Enforce admin password expiry, forced resets, reuse history, and breach (HIBP) checks across your Capell panels — configured from one settings screen, no code.” Screenshot media is closed again: capell.json promotes six real Capell screenshot runner captures for the Password Policy settings page, forced-change form, and Users-table policy column in light/dark mode. The composer description, capell.json description/summary, README, and overview now agree on the shipped admin password controls.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”-
Shipped 2026-06-06: screenshot contract closed with real runner captures. The settings page is now registered as a real admin page for bridge and legacy hosts, the screenshot contract targets that route plus the forced-change form and Users table, and
capell.jsonpromotes six Capell screenshot runner PNGs in light/dark mode. —docs/screenshots.json,capell.json,src/Bridges/PasswordPolicyAdminBridge.php,src/Providers/PasswordPolicyServiceProvider.php,src/Filament/Pages/PasswordPolicySettingsPage.php— Done -
Configurable complexity in the validator is wired — settings now cover minimum length, mixed case, numbers, and symbols, and
ValidatePasswordChangeActionbuilds Laravel’sPasswordrule from those settings. —src/Actions/ValidatePasswordChangeAction.php+src/Filament/Settings/PasswordPolicySettingsSchema.php— Done -
Shipped 2026-06-06: extension-surface registration is single-owner. Bridge-capable hosts now let
PasswordPolicyAdminBridgeregister the Extensions settings surface; the service-provider fallback only runs for legacy non-bridge hosts. —src/Providers/PasswordPolicyServiceProvider.php+src/Bridges/PasswordPolicyAdminBridge.php— Done -
Shipped 2026-06-06: password history pruning.
RecordPasswordHistoryActionnow trims old rows for the user after inserting a new hash, keeping the most recent configuredpasswordHistoryCountentries ordered by creation/id. —src/Actions/RecordPasswordHistoryAction.php— S -
Admin edit history ownership is single-path —
PasswordPolicyUserFormExtendervalidates before save, stashes the previous hash, and records exactly one history row fromafterSave()only after Filament confirms the password changed.UpdatePasswordActionremains the owner for the forced-change flow. —src/Filament/Extenders/PasswordPolicyUserFormExtender.php+tests/Feature/PasswordPolicyAdminTest.php— Done -
password_changed_atinstall safety is closed — the install migration backfills existing users when the column is added, and policy evaluation no longer treats a missing legacy timestamp as expired by itself. —database/migrations/2026_05_10_190863_01_add_password_policy_columns_to_users_table.php+tests/Unit/PasswordPolicySettingsSchemaTest.php— Done -
History reuse check ignores the configured count for the live hash — reuse always includes the current password hash plus up to
passwordHistoryCounthistory rows; fine, butmax(1, …)silently rewrites a0setting to1. Validate the setting at the form layer (min 1 already in schema) and drop the silent clamp so behaviour matches the configured value. —src/Actions/ValidatePasswordChangeAction.php:66— S -
Add
down()to the settings migration — the Spatie settings migration only definesup(); rollbacks leave orphanedpassword_policy.*rows. —database/settings/2026_05_10_190864_01_create_password_policy_settings.php— S -
canAccess()on the forced-change page is any-authenticated —ForcedPasswordChangePage::canAccess()returns true for anyAuthenticatable, so the page is reachable in every panel even when the policy is disabled or the user is compliant. Gate onEvaluatePasswordPolicyActionstatus (or at leastforce_change_enabled || password_expiry_enabled). —src/Filament/Pages/ForcedPasswordChangePage.php:43— S -
Shipped 2026-06-06: schema probes use
RuntimeSchemaState. Password Policy actions, form persistence, posture reporting, and health diagnostics now route table/column checks through the shared cached schema state instead of rawSchemafacade probes. —src/Actions/*.php,src/Filament/Extenders/PasswordPolicyUserFormExtender.php,src/Health/PasswordPolicyHealthCheck.php— Done -
ForcedPasswordChangePageredirect target is hand-built —redirect('/' . trim($panelPath, '/'))rebuilds the panel URL by hand; useFilament::getCurrentPanel()?->getUrl()/ the panel home route to survive path/prefix changes. —src/Filament/Pages/ForcedPasswordChangePage.php:108— S
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”Tied to capabilities[]: password-policy, password-policy-admin.
- Configurable complexity rules (closed in current slice). The package now exposes minimum length, mixed-case, number, and symbol settings and wires them into
Password::min(...)->mixedCase()->numbers()->symbols()inValidatePasswordChangeAction. Keep future register/reset integration aligned with this same settings boundary. — Differentiator-critical: this is the headline feature buyers expect. - Shipped 2026-06-06: reusable host-auth password rule.
PasswordPolicyRulenow exposes the same settings-backed complexity, compromised-password, and optional user-history checks used by the admin action, anddocs/fortify.mdshows host FortifyCreateNewUser/ResetUserPasswordopt-in snippets without adding a hard Fortify dependency. — Table stakes boundary closed. - Account lockout / throttle on failed logins. No brute-force protection (rate-limit, temporary lockout, lockout counter columns). A natural premium capability and a common compliance requirement. — Differentiator.
- Password expiry notifications. Expiry only acts at request time (redirect). No “your password expires in N days” email/notification ahead of lockout. — Differentiator.
- HIBP hardening + admin toggle context. The compromised check delegates to Laravel’s
uncompromised()(HIBP k-anonymity, range API). There is no timeout, failure fallback (fail-open vs fail-closed), result caching, or documentation of the privacy model. Add config for timeout + fail mode and document k-anonymity so security-conscious buyers trust it. — Table stakes + trust. - Done/Shipped: console command surface.
capell.jsonnow declares the console surface and doctor command, and the service provider registerscapell:password-policy:expire-stale,capell:password-policy:require-change,capell:password-policy:prune-history, andcapell:password-policy:doctor. Commands reuse existing Actions/health checks, support dry-run where mutation is broad, and keep history pruning inPrunePasswordHistoryAction. —src/Console/Commands/*,src/Actions/PrunePasswordHistoryAction.php,src/Providers/PasswordPolicyServiceProvider.php,capell.json - Shipped 2026-06-06: password lifecycle subscriber hooks. Password changes, expiry detection, and forced-change marking now notify
CapellCore::subscriberManager()with package-scoped event names and typed context objects;docs/overview.mddocuments the listener contract for Login Audit, 2FA, and other security packages. — Differentiator + cross-sell enabler closed. - Per-role / per-panel policy scoping. Settings are global. Enterprises typically want stricter rules for admins than editors, or different rules per panel. — Differentiator (enterprise tier).
- Done/Shipped: real health-check assertions.
PasswordPolicyHealthCheck::runDiagnostics()now returns concrete install-health checks for forced-change, expiry, and history persistence, andpassed()fails when an enabled control is missing its backing column/table. Existing tests cover installed, disabled, missing-history-table, missing-forced-change-column, and missing-expiry-column cases. —src/Health/PasswordPolicyHealthCheck.php,tests/Unit/PasswordPolicyHealthCheckTest.php
4. Issues / Risks
Section titled “4. Issues / Risks”- Compromised-password (HIBP) path now has regression coverage.
PasswordPolicySettingsSchemaTestfakes the pwned-passwords range API, prevents stray requests, and proves a breached password is rejected. —tests/Unit/PasswordPolicySettingsSchemaTest.php— Closed - The middleware class itself is never unit-tested. Redirect behaviour is asserted at the Action level (
EvaluatePasswordPolicyAction→it('redirects flagged admin users…')), butEnsurePasswordPolicyComplianceis never instantiated;middleware/authMiddlewarehave 0 test hits. The allowed-route logic (logout route, change-password path matching via$request->is()) and the “no redirect when compliant” branch are unverified. A regression here locks admins out of the panel. —src/Http/Middleware/EnsurePasswordPolicyCompliance.php— High - Expiry enablement lockout risk is closed for the shipped path. The install migration backfills existing users when adding
password_changed_at, and missing legacy timestamps no longer force expiry redirects by themselves. —src/Actions/EvaluatePasswordPolicyAction.php+database/migrations/2026_05_10_190863_01_add_password_policy_columns_to_users_table.php— Closed - Enforcement surface is narrow / partially advertised. Rules apply to Filament admin forms + forced-change page only. The package name promises a “policy” but cannot enforce on register/reset (§3). Document this boundary explicitly and ship the reusable rule. — Medium
- Closed: core password validation failures use package i18n.
ValidatePasswordChangeActionnow passes package-owned messages for required, confirmation, minimum length, mixed-case, numbers, symbols, and compromised-password failures. —src/Actions/ValidatePasswordChangeAction.php+resources/lang/en/validation.php— Closed - Closed: history table is pruned per user.
RecordPasswordHistoryActionnow keeps only the configured number of recent history rows after recording a password change, bounding future validation work. —src/Actions/RecordPasswordHistoryAction.php— Medium - Performance budget plausibility narrowed. Raw schema probes now use
RuntimeSchemaState(§2), reducing repeated metadata queries. The remaining risk is the per-rowmust_change_passwordpolicy/Gatechecks in the table extender (canMarkUserper record); no test asserts the query count. —capell.jsonperformance+src/Filament/Extenders/PasswordPolicyUserTableExtender.php:111— Medium config('capell-password-policy.enabled')kill-switch is coarse and untested. Setting it false skips all registration (settings, surfaces, middleware) — including the forced-change page route — which could strand already-flagged users mid-flow. No test covers the disabled path. —src/Providers/PasswordPolicyServiceProvider.php:38— Low- Manifest/doc drift is mostly closed. composer and Capell descriptions now match, README is populated, and the unimplemented console surface/capability was removed. The remaining low-level follow-up is to confirm whether
deferredContributions: ["admin-page"]should stay while the Extensions modal is the live settings path. —composer.json,README.md,capell.json— Low
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Critique. Marketplace and Composer copy now agree on the shipped admin password controls: expiry, forced resets, reuse history, breach checks, and one settings screen. README and overview are populated. The required marketplace screenshot contract is now complete, so the remaining buyer-facing depth is feature expansion rather than visual proof.
Improved summary (1 sentence):
Enforce admin password expiry, forced resets, reuse history, and breach (HIBP) checks across your Capell panels — configured from one settings screen, no code.
Improved description (3–4 sentences):
Password Policy gives Capell administrators enterprise-grade account controls without touching code. Toggle password expiry windows, force flagged users to reset on next login, block recently reused passwords, and reject known-breached passwords via Have I Been Pwned — all from a single admin settings page. Policy state lives on the users table with full table columns, filters, and one-click “require change” row and bulk actions. Built on Capell Actions so host apps can reuse the same validation rules in their own registration and reset flows.
Screenshot/media status. capell.json lists the extension card plus six real Capell screenshot runner captures for the settings page, forced-change form, and Users-table policy column in light/dark mode. Add a deeper complexity-rules or lockout screenshot once the future §3 feature rows land.
Pricing / tier / bundle positioning. tier: premium, bundle: operations, group Capell Operations, license paid, certification first-party, support priority — appropriate for a security control. To justify premium over Laravel’s free built-ins, the differentiators (lockout, expiry notifications, per-role scoping, breach checks with admin toggle) in §3 need to ship; today the core (expiry + history + uncompromised) is thin for a paid security add-on.
Cross-sell. Manifest deps are only the two Capell platform packages, so cross-sell is by workflow not by dependency. docs/README.md already points at Login Audit and Diagnostics as neighbours — lean into that as an Extension Suite: pair Password Policy with login-audit (who reset/was-locked, when) and a privacy-center (data-subject / credential hygiene) package. Shipped password lifecycle events (§3) make login-audit a natural attach. Position the trio as a “Capell Account Security” suite bundle.
Differentiators / value props / target buyer. Target buyer: agencies and in-house teams running Capell for clients with compliance needs (SOC 2 / ISO 27001 / internal IT policy) who need auditable admin password controls out of the box. Value props: zero-code config, breach-aware, reusable in custom auth flows, and (once built) lockout + expiry notifications + per-role rules.
Keywords/tags (8–12): password policy, password expiry, forced password change, password history, compromised password, have i been pwned, hibp, account security, admin security, compliance, password reuse, filament security.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
Test HIBP/uncompromised path with Http::fake() + stray-request guard | Done | S | High | §4 |
Backfill password_changed_at on install (or treat null as non-expired) | Done | M | High | §2, §4 |
| Add direct middleware tests (redirect, allowed-route, compliant no-op) | Done | S | High | §4 |
| Reconcile manifest drift: composer↔capell.json desc, fill README, resolve console capability | Done | S | Medium | §4, §5 |
| Generate real Capell runner marketplace screenshots for settings, forced-change, and Users-table policy columns | Done | S | High | §5 |
| Shipped: rewrite marketplace summary/description | Done | S | High | §5 |
| Add configurable complexity rules (length/case/number/symbol) + wire into validator | Done | M | High | §3, §2 |
| Fix double/duplicate history write on admin edit; record once post-hash | Done | M | Medium | §2 |
| De-duplicate extension-surface registration (bridge vs provider) | Done | S | Medium | §2 |
Prune password_policy_password_histories to configured count | Done | S | Medium | §2, §4 |
Ship reusable PasswordPolicyRule + Fortify register/reset hooks | Done | M | High | §3 |
Route Action schema probes through RuntimeSchemaState; assert admin query budget | Done | M | Medium | §2, §4 |
| Translate core password validation failures into package i18n namespace | Done | S | Medium | §4 |
| Emit password lifecycle events for login-audit / 2FA cross-sell | Done | M | Medium | §3, §5 |
| Account lockout / login throttle + expiry-warning notifications | Later | L | High | §3 |
| Per-role / per-panel policy scoping | Later | L | Medium | §3 |
| Done/Shipped: Real health-check assertions (enabled-but-not-installed → fail) | Done | M | Medium | §3, §4 |
| Done/Shipped: Add Artisan console commands (expire-stale, require-change, prune-history, doctor) | Done | M | Medium | §3 |