Skip to content

Password Policy — Improvement & Growth Plan

Package: capell-app/password-policy · Kind: package · Tier: premium · Product group: Capell Operations · Bundle: operations · Status: Draft

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.

  • 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.json promotes 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.phpDone

  • Configurable complexity in the validator is wired — settings now cover minimum length, mixed case, numbers, and symbols, and ValidatePasswordChangeAction builds Laravel’s Password rule from those settings. — src/Actions/ValidatePasswordChangeAction.php + src/Filament/Settings/PasswordPolicySettingsSchema.phpDone

  • Shipped 2026-06-06: extension-surface registration is single-owner. Bridge-capable hosts now let PasswordPolicyAdminBridge register the Extensions settings surface; the service-provider fallback only runs for legacy non-bridge hosts. — src/Providers/PasswordPolicyServiceProvider.php + src/Bridges/PasswordPolicyAdminBridge.phpDone

  • Shipped 2026-06-06: password history pruning. RecordPasswordHistoryAction now trims old rows for the user after inserting a new hash, keeping the most recent configured passwordHistoryCount entries ordered by creation/id. — src/Actions/RecordPasswordHistoryAction.phpS

  • Admin edit history ownership is single-pathPasswordPolicyUserFormExtender validates before save, stashes the previous hash, and records exactly one history row from afterSave() only after Filament confirms the password changed. UpdatePasswordAction remains the owner for the forced-change flow. — src/Filament/Extenders/PasswordPolicyUserFormExtender.php + tests/Feature/PasswordPolicyAdminTest.phpDone

  • password_changed_at install 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.phpDone

  • History reuse check ignores the configured count for the live hash — reuse always includes the current password hash plus up to passwordHistoryCount history rows; fine, but max(1, …) silently rewrites a 0 setting to 1. 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:66S

  • Add down() to the settings migration — the Spatie settings migration only defines up(); rollbacks leave orphaned password_policy.* rows. — database/settings/2026_05_10_190864_01_create_password_policy_settings.phpS

  • canAccess() on the forced-change page is any-authenticatedForcedPasswordChangePage::canAccess() returns true for any Authenticatable, so the page is reachable in every panel even when the policy is disabled or the user is compliant. Gate on EvaluatePasswordPolicyAction status (or at least force_change_enabled || password_expiry_enabled). — src/Filament/Pages/ForcedPasswordChangePage.php:43S

  • 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 raw Schema facade probes. — src/Actions/*.php, src/Filament/Extenders/PasswordPolicyUserFormExtender.php, src/Health/PasswordPolicyHealthCheck.phpDone

  • ForcedPasswordChangePage redirect target is hand-builtredirect('/' . trim($panelPath, '/')) rebuilds the panel URL by hand; use Filament::getCurrentPanel()?->getUrl() / the panel home route to survive path/prefix changes. — src/Filament/Pages/ForcedPasswordChangePage.php:108S

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() in ValidatePasswordChangeAction. 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. PasswordPolicyRule now exposes the same settings-backed complexity, compromised-password, and optional user-history checks used by the admin action, and docs/fortify.md shows host Fortify CreateNewUser / ResetUserPassword opt-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.json now declares the console surface and doctor command, and the service provider registers capell:password-policy:expire-stale, capell:password-policy:require-change, capell:password-policy:prune-history, and capell:password-policy:doctor. Commands reuse existing Actions/health checks, support dry-run where mutation is broad, and keep history pruning in PrunePasswordHistoryAction. — 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.md documents 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, and passed() 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
  • Compromised-password (HIBP) path now has regression coverage. PasswordPolicySettingsSchemaTest fakes the pwned-passwords range API, prevents stray requests, and proves a breached password is rejected. — tests/Unit/PasswordPolicySettingsSchemaTest.phpClosed
  • The middleware class itself is never unit-tested. Redirect behaviour is asserted at the Action level (EvaluatePasswordPolicyActionit('redirects flagged admin users…')), but EnsurePasswordPolicyCompliance is never instantiated; middleware / authMiddleware have 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.phpHigh
  • 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.phpClosed
  • 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. ValidatePasswordChangeAction now 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.phpClosed
  • Closed: history table is pruned per user. RecordPasswordHistoryAction now keeps only the configured number of recent history rows after recording a password change, bounding future validation work. — src/Actions/RecordPasswordHistoryAction.phpMedium
  • Performance budget plausibility narrowed. Raw schema probes now use RuntimeSchemaState (§2), reducing repeated metadata queries. The remaining risk is the per-row must_change_password policy/Gate checks in the table extender (canMarkUser per record); no test asserts the query count. — capell.json performance + src/Filament/Extenders/PasswordPolicyUserTableExtender.php:111Medium
  • 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:38Low
  • 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.jsonLow

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.

ItemBucketEffortImpactSection ref
Test HIBP/uncompromised path with Http::fake() + stray-request guardDoneSHigh§4
Backfill password_changed_at on install (or treat null as non-expired)DoneMHigh§2, §4
Add direct middleware tests (redirect, allowed-route, compliant no-op)DoneSHigh§4
Reconcile manifest drift: composer↔capell.json desc, fill README, resolve console capabilityDoneSMedium§4, §5
Generate real Capell runner marketplace screenshots for settings, forced-change, and Users-table policy columnsDoneSHigh§5
Shipped: rewrite marketplace summary/descriptionDoneSHigh§5
Add configurable complexity rules (length/case/number/symbol) + wire into validatorDoneMHigh§3, §2
Fix double/duplicate history write on admin edit; record once post-hashDoneMMedium§2
De-duplicate extension-surface registration (bridge vs provider)DoneSMedium§2
Prune password_policy_password_histories to configured countDoneSMedium§2, §4
Ship reusable PasswordPolicyRule + Fortify register/reset hooksDoneMHigh§3
Route Action schema probes through RuntimeSchemaState; assert admin query budgetDoneMMedium§2, §4
Translate core password validation failures into package i18n namespaceDoneSMedium§4
Emit password lifecycle events for login-audit / 2FA cross-sellDoneMMedium§3, §5
Account lockout / login throttle + expiry-warning notificationsLaterLHigh§3
Per-role / per-panel policy scopingLaterLMedium§3
Done/Shipped: Real health-check assertions (enabled-but-not-installed → fail)DoneMMedium§3, §4
Done/Shipped: Add Artisan console commands (expire-stale, require-change, prune-history, doctor)DoneMMedium§3