Description
Add a scheduled job that periodically scans the user_ table for rows whose passwordEncrypted boolean is false (i.e., the password is sitting in password_ as plaintext) and rewrites them with a secure hash via PasswordFactoryProxy.generateHash, then flips passwordEncrypted = true.
Why this is needed
Plaintext passwords can land in the user_ table from several paths:
- Legacy migration data and seed inserts (the bootstrap SQL contains
passwordEncrypted = false rows by design).
- Bulk user imports that bypass the standard
UserAPI.save(..., validatePassword=true) path.
- Manual SQL inserts during recovery or environment provisioning.
- Older code paths that set the password but never set the encrypted flag.
A periodic sweep is a defense-in-depth control: it guarantees that no matter how a plaintext password lands in the column, it gets hashed on the next tick rather than persisting forever.
Behaviour
- Runs every minute by default (configurable cron).
- Implemented as a Quartz
StatefulJob so concurrent firings cannot overlap.
- Uses
PasswordFactoryProxy.generateHash — the same hashing utility used everywhere else in the platform (authentication continues to work because authPassword accepts the resulting hash).
- Skips rows where
password_ is null.
- Logs per-cycle counts (only when there is work to do — silent when the table is clean).
Configuration
| Property |
Default |
Effect |
ENABLE_ENCRYPT_PLAIN_PASSWORDS_JOB |
true |
Kill switch. Checked at startup (job is not scheduled if false) and at every firing (so it can be flipped at runtime without a restart). |
ENCRYPT_PLAIN_PASSWORDS_CRON_EXPRESSION |
0 0/1 * * * ? |
Standard Quartz cron expression. |
Acceptance Criteria
Additional Context
This is a backend-only change. No API contract, schema, or frontend touched.
Description
Add a scheduled job that periodically scans the
user_table for rows whosepasswordEncryptedboolean isfalse(i.e., the password is sitting inpassword_as plaintext) and rewrites them with a secure hash viaPasswordFactoryProxy.generateHash, then flipspasswordEncrypted = true.Why this is needed
Plaintext passwords can land in the
user_table from several paths:passwordEncrypted = falserows by design).UserAPI.save(..., validatePassword=true)path.A periodic sweep is a defense-in-depth control: it guarantees that no matter how a plaintext password lands in the column, it gets hashed on the next tick rather than persisting forever.
Behaviour
StatefulJobso concurrent firings cannot overlap.PasswordFactoryProxy.generateHash— the same hashing utility used everywhere else in the platform (authentication continues to work becauseauthPasswordaccepts the resulting hash).password_ is null.Configuration
ENABLE_ENCRYPT_PLAIN_PASSWORDS_JOBtruefalse) and at every firing (so it can be flipped at runtime without a restart).ENCRYPT_PLAIN_PASSWORDS_CRON_EXPRESSION0 0/1 * * * ?Acceptance Criteria
EncryptPlainPasswordsJobexists incom.dotmarketing.quartz.jobimplementingStatefulJob.DotInitSchedulerand runs every minute by default.ENABLE_ENCRYPT_PLAIN_PASSWORDS_JOBworks at both startup and runtime.Additional Context
This is a backend-only change. No API contract, schema, or frontend touched.