fix(config): validate system_config on runtime read + align cache TTL (#13)#19
fix(config): validate system_config on runtime read + align cache TTL (#13)#19SAY-5 wants to merge 2 commits intofells-code:mainfrom
Conversation
…fells-code#13) Closes fells-code#13. Two related bugs: 1. `getSystemConfig` returned `Object.fromEntries(rows)` cast as SystemConfig with no schema validation. The admin read handler already validates via SystemConfigSchema.safeParse, but the runtime hot path (used by every authenticated request) trusted whatever shape the DB happened to contain. A manual DB edit, an out-of-band migration, or schema drift would silently produce shape-mismatched config for downstream auth code. 2. The cache TTL constant was `300_000` ms with the comment '// 30 seconds'. The actual value is 5 minutes, not 30 seconds. Schema-validate on every runtime load via SystemConfigSchema. safeParse. On failure: log the zod issues array, throw 'System configuration is invalid', and leave `cachedConfig` as null so the next request retries rather than serving stale tainted data. Aligns the cache TTL constant to `5 * 60 * 1000` with a matching '// Five minutes' comment, and exports it so the test suite can reference the actual value.
| const CACHE_TTL_MS = 300_000; // 30 seconds | ||
| // Cached, schema-validated copy of the system config row set. Only | ||
| // assigned after `SystemConfigSchema.safeParse(...)` succeeds, so every | ||
| // cache hit is guaranteed valid. See issue #13. |
There was a problem hiding this comment.
No self explanatory comments please. Unless you truly feel a developer looking at the code would NEED to know this.
Bccorb
left a comment
There was a problem hiding this comment.
Overall, please no useless comments. This doesn't look to have been tested with the actual auth system.
| // Five minutes — keep the comment in sync with the value. The previous | ||
| // "// 30 seconds" comment did not match the actual 300_000 ms (#13). | ||
| export const CACHE_TTL_MS = 5 * 60 * 1000; | ||
|
|
||
| /** | ||
| * Load the system config from the DB and validate it against | ||
| * `SystemConfigSchema`. The runtime read path now refuses to hand out | ||
| * an unvalidated cast — if a row has become tainted (manual DB edit, | ||
| * out-of-band migration, schema drift) the call throws with a clear | ||
| * error rather than silently returning shape-mismatched data to | ||
| * downstream auth code. | ||
| * | ||
| * Closes https://github.com/fells-code/seamless-auth-api/issues/13. | ||
| */ |
There was a problem hiding this comment.
No self explanatory comments please. Unless you truly feel a developer looking at the code would NEED to know this.
This is the incorrect cache for updating.
| const configObject = Object.fromEntries(rows.map((row) => [row.key, row.value])); | ||
|
|
||
| const parsed = SystemConfigSchema.safeParse(configObject); | ||
| if (!parsed.success) { | ||
| logger.error('System config failed schema validation on runtime read', { | ||
| issues: parsed.error.issues, | ||
| }); | ||
| throw new Error('System configuration is invalid'); | ||
| } | ||
|
|
||
| cachedConfig = Object.fromEntries(rows.map((row) => [row.key, row.value])); | ||
|
|
||
| cachedConfig = parsed.data; |
There was a problem hiding this comment.
I testing my .env file did still let me pass improper config values. For example I passed test to an origins field.
| // Build a full set of valid `system_config` rows that satisfies | ||
| // `SystemConfigSchema`. The runtime read path now validates against | ||
| // the schema (#13), so partial `{ app_name: ... }` payloads no longer | ||
| // round-trip — every row must be present. |
There was a problem hiding this comment.
No self explanatory comments please. Unless you truly feel a developer looking at the code would NEED to know this.
| vi.spyOn(Date, 'now') | ||
| .mockReturnValueOnce(Date.now() + 1) | ||
| .mockReturnValueOnce(Date.now() + 400_000); // > TTL | ||
| .mockReturnValueOnce(Date.now() + 6 * 60 * 1000); // > 5 min TTL |
There was a problem hiding this comment.
No self explanatory comments please. Unless you truly feel a developer looking at the code would NEED to know this.
| // Regression for https://github.com/fells-code/seamless-auth-api/issues/13. | ||
| // The runtime read path must schema-validate the rows it pulls from | ||
| // the DB and refuse to hand out tainted config. Partial / malformed | ||
| // rows that previously slipped through the bare cast now throw. |
There was a problem hiding this comment.
No self explanatory comments please. Unless you truly feel a developer looking at the code would NEED to know this.
| it('throws when DB rows fail schema validation', async () => { | ||
| const { SystemConfig } = await import('../../../src/models/systemConfig'); | ||
|
|
||
| // Drop a required field — `default_roles` — so schema validation fails. | ||
| const tainted = validRows().filter((row) => row.key !== 'default_roles'); | ||
| (SystemConfig.findAll as any).mockResolvedValue(tainted); | ||
|
|
||
| const { getSystemConfig } = await import('../../../src/config/getSystemConfig'); | ||
|
|
||
| await expect(getSystemConfig()).rejects.toThrow('System configuration is invalid'); | ||
| }); |
There was a problem hiding this comment.
Can you explain this test to me? Also lets add cases for each field.
| it('does not cache a tainted result', async () => { | ||
| const { SystemConfig } = await import('../../../src/models/systemConfig'); | ||
|
|
||
| const tainted = validRows().filter((row) => row.key !== 'app_name'); | ||
| const valid = validRows({ app_name: 'Recovered' }); | ||
| (SystemConfig.findAll as any) | ||
| .mockResolvedValueOnce(tainted) | ||
| .mockResolvedValueOnce(valid); | ||
|
|
||
| const { getSystemConfig } = await import('../../../src/config/getSystemConfig'); | ||
|
|
||
| await expect(getSystemConfig()).rejects.toThrow(); | ||
|
|
||
| // After the operator fixes the row, the next call must hit the DB | ||
| // again (no stale "tainted" entry sitting in cache) and succeed. | ||
| const result = await getSystemConfig(); | ||
| expect(result.app_name).toBe('Recovered'); | ||
| expect(SystemConfig.findAll).toHaveBeenCalledTimes(2); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
I tested this and it does not appear to be correct. I can used an incorrect formatted value parsed at runtime
Per @Bccorb's review: - Drop self-explanatory comments from `getSystemConfig.ts` and the test file — code reads, why-context lives in commit/PR. - Replace single "throws on schema validation failure" test with a per-field `it.each` covering every required-field rule: app_name min length, role-pattern regex, TTL format, positive-int rate_limit, non-negative delay_after, non-empty rpid, and origins URL / type / non-empty cases (including the literal `'test'` the reviewer reported as slipping through — schema does reject it). - Rename "does not cache a tainted result" to spell out the recovery flow it exercises (operator fixes row → next call reloads).
|
Round-2 in 35edc47: Comments: dropped the self-explanatory comments from Per-field validation tests: replaced the single "throws on schema validation failure" with an On the
|
Bccorb
left a comment
There was a problem hiding this comment.
Feature still does not work when running the system
|
Could you share the failing scenario and error output? Happy to debug — what's the actual symptom, or what setup are you running? I tested the cache-TTL path locally with the existing Jest suite but didn't have the live auth system to validate end-to-end. |
Closes #13.
Two related bugs
getSystemConfigreturnedObject.fromEntries(rows)cast asSystemConfigwith nosafeParse. The admin read handler (getSystemConfigHandlerinsrc/controllers/systemConfig.ts:105) already validates againstSystemConfigSchema, but the runtime hot path — used by every authenticated request — trusted whatever shape the DB happened to contain. A manual DB edit, an out-of-band migration, or schema drift would silently produce shape-mismatched config for downstream auth code.CACHE_TTL_MS = 300_000was commented// 30 seconds. The actual value is five minutes. Issue [Feature]: Validate system_config in the runtime read path #13 explicitly calls this out.Fix
SystemConfigSchema.safeParse. On failure: log the zod issues array, throw'System configuration is invalid', and leavecachedConfigasnullso the next request retries against the (potentially fixed) DB rather than serving stale tainted data.5 * 60 * 1000with a matching// Five minutescomment.CACHE_TTL_MSso the test suite can reference the actual constant rather than copy-pasting the magic number.Behavior
parsed.data(unchanged from caller's POV)System configuration is invalid// 30 secondsover 300_000 ms// Five minutesover5 * 60 * 1000Test plan
Updated
tests/unit/config/getSystemConfig.spec.ts:validRows()helper that emits a full schema-valid config (the previous partial{ app_name: 'TestApp' }rows no longer round-trip through validation).throws when DB rows fail schema validation— exact case from the issue: a row dropped from the result set rejects withSystem configuration is invalid.does not cache a tainted result— pins that a failed validation doesn't poison the cache; the next call hits the DB again and succeeds once the operator fixes the row.Local results:
npm run test -- tests/unit/config/getSystemConfig.spec.ts→ 6 passed.npm run test -- tests/unit/config/→ 18 passed (no regressions in sibling suites).Notes