Commit 3139ad7
feat(security): periodic job to encrypt plaintext passwords in user_ table (#35767)
## Proposed Changes
Adds **`EncryptPlainPasswordsJob`** — a Quartz `StatefulJob` that runs
every 5 minutes, scans the `user_` table for rows whose
`passwordEncrypted` flag is `false`, hashes the cleartext value via
`PasswordFactoryProxy.generateHash`, and flips the flag to `true`.
Defense-in-depth against any code path that lands a plaintext password
in `user_.password_` — migrations, bulk imports, manual SQL recovery, or
older code that set the password without the encrypted flag. Once the
job ticks, the row is hashed using the same utility as the rest of the
platform, so existing login (`authPassword`) continues to work
transparently.
## Files Touched
| File | Change |
| --- | --- |
|
`dotCMS/src/main/java/com/dotmarketing/quartz/job/EncryptPlainPasswordsJob.java`
| **New.** `StatefulJob` implementation. |
| `dotCMS/src/main/java/com/dotmarketing/init/DotInitScheduler.java` |
Registers the new job (mirrors `FreeServerFromClusterJob` pattern). |
|
`dotcms-integration/src/test/java/com/dotmarketing/quartz/job/EncryptPlainPasswordsJobTest.java`
| **New.** Five integration tests. |
## Configuration
| Property | Default | Effect |
| --- | --- | --- |
| `ENABLE_ENCRYPT_PLAIN_PASSWORDS_JOB` | `true` | Kill switch. Checked
at startup (job not scheduled if `false`) **and** at every firing — flip
at runtime without a restart. |
| `ENCRYPT_PLAIN_PASSWORDS_CRON_EXPRESSION` | `0 0/5 * * * ?` | Standard
Quartz cron. |
## Why a periodic job rather than a one-off migration
The risk of a fresh plaintext row landing in `user_` is non-zero on a
live system (admin tooling, recovery scripts, bulk imports that bypass
`UserAPI`). A standing sweep catches them within one minute regardless
of how they got there.
The query is cheap: `passwordEncrypted = false` is an extremely
selective predicate, so in steady state the job does an index/sequential
check that finds zero rows and returns immediately. A partial index
`CREATE INDEX ... ON user_ (userId) WHERE passwordEncrypted = false`
would be the right escalation if perf ever becomes a concern, but is
unnecessary today.
## Test Plan
Integration tests (`EncryptPlainPasswordsJobTest`):
- [x] **Happy path** — plaintext row gets hashed;
`authPassword(plaintext, storedHash)` returns `AUTHENTICATED`.
- [x] **Already encrypted row** — left untouched (no double-hashing).
- [x] **Null password** — skipped (no UPDATE issued).
- [x] **Multiple rows** — all hashed in a single pass.
- [x] **Disabled flag** — `ENABLE_ENCRYPT_PLAIN_PASSWORDS_JOB=false`
makes the firing a no-op.
Run locally:
```bash
./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=EncryptPlainPasswordsJobTest
```
## Rollback safety
Pure additive — new class, new scheduler registration, new test. No
schema change, no API contract change, no frontend touched. If anything
misbehaves in production, flip
`ENABLE_ENCRYPT_PLAIN_PASSWORDS_JOB=false` and the job becomes a no-op
immediately; revert the commit for a full backout.
Refs #35766
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 531ae0a commit 3139ad7
4 files changed
Lines changed: 457 additions & 0 deletions
File tree
- dotCMS/src/main/java/com/dotmarketing
- init
- quartz/job
- dotcms-integration/src/test/java/com
- dotcms
- dotmarketing/quartz/job
Lines changed: 45 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
301 | 302 | | |
302 | 303 | | |
303 | 304 | | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
304 | 349 | | |
305 | 350 | | |
306 | 351 | | |
| |||
Lines changed: 129 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| 164 | + | |
164 | 165 | | |
165 | 166 | | |
166 | 167 | | |
| |||
471 | 472 | | |
472 | 473 | | |
473 | 474 | | |
| 475 | + | |
474 | 476 | | |
475 | 477 | | |
476 | 478 | | |
| |||
0 commit comments