Skip to content

Commit b35c5b3

Browse files
gabemonteroclaude
andcommitted
fix(boost): prevent data loss on secret rotation and add auth policy
- validateStoredValues: separate decryption errors from validation errors. Rows that fail decryption (rotated secret) are kept intact with a warning instead of being deleted. Only rows that fail Zod schema validation after successful decryption are removed. - Add httpRouter.addAuthPolicy for /config/status endpoint to match the pattern used by all other endpoints in the codebase. - Add tests for both new behaviors (2 new test cases, 79 total). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com>
1 parent 5f91eb5 commit b35c5b3

3 files changed

Lines changed: 74 additions & 9 deletions

File tree

workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,50 @@ describe('AdminConfigService', () => {
351351
expect(removed).toContain('boost.nonexistent.field');
352352
});
353353

354+
it('keeps encrypted rows when decryption fails (rotated secret)', async () => {
355+
// Write a sensitive field with the current secret
356+
await service.setOverride(
357+
'boost.devSpaces.credentials',
358+
'my-secret-token',
359+
);
360+
361+
// Create a service with a different secret
362+
const database: DatabaseService = {
363+
getClient: async () => mockKnex,
364+
} as unknown as DatabaseService;
365+
const rotatedService = new AdminConfigService({
366+
database,
367+
logger,
368+
encryptionSecret: 'different-secret',
369+
});
370+
371+
const removed = await rotatedService.validateStoredValues();
372+
expect(removed).not.toContain('boost.devSpaces.credentials');
373+
// Row should still exist
374+
const row = mockKnex._rows.find(
375+
(r: { key: string }) => r.key === 'boost.devSpaces.credentials',
376+
);
377+
expect(row).toBeDefined();
378+
expect(logger.warn).toHaveBeenCalledWith(
379+
expect.stringContaining('decryption failed'),
380+
);
381+
});
382+
383+
it('removes corrupt JSON rows during validation', async () => {
384+
mockKnex._rows.push({
385+
key: 'boost.model.baseUrl',
386+
value: '<<<not-json>>>',
387+
schema_version: 1,
388+
updated_at: new Date().toISOString(),
389+
});
390+
391+
const removed = await service.validateStoredValues();
392+
expect(removed).toContain('boost.model.baseUrl');
393+
expect(logger.warn).toHaveBeenCalledWith(
394+
expect.stringContaining('corrupt JSON'),
395+
);
396+
});
397+
354398
it('removes values that fail validation', async () => {
355399
// Insert invalid URL directly
356400
mockKnex._rows.push({

workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -308,19 +308,36 @@ export class AdminConfigService {
308308
);
309309
}
310310

311-
// Re-validate the stored value
311+
// Decrypt sensitive fields before validation
312+
let rawValue: unknown;
312313
try {
313-
let rawValue: unknown = JSON.parse(row.value);
314-
315-
// Decrypt sensitive fields for validation
316-
if (
317-
isSensitiveField(key) &&
318-
typeof rawValue === 'string' &&
319-
this.encryptionSecret
320-
) {
314+
rawValue = JSON.parse(row.value);
315+
} catch {
316+
this.logger.warn(
317+
`Removing config override "${key}" — corrupt JSON (schema version ${row.schema_version})`,
318+
);
319+
await knex<AdminConfigRow>(TABLE_NAME).where({ key }).delete();
320+
removedKeys.push(key);
321+
continue;
322+
}
323+
324+
if (
325+
isSensitiveField(key) &&
326+
typeof rawValue === 'string' &&
327+
this.encryptionSecret
328+
) {
329+
try {
321330
rawValue = decryptValue(rawValue, this.encryptionSecret);
331+
} catch {
332+
this.logger.warn(
333+
`Cannot validate sensitive field "${key}" — decryption failed (secret may have been rotated). Keeping row intact.`,
334+
);
335+
continue;
322336
}
337+
}
323338

339+
// Re-validate the decrypted value against the Zod schema
340+
try {
324341
validateConfigValue(key, rawValue);
325342
} catch (error) {
326343
this.logger.warn(

workspaces/boost/plugins/boost-backend/src/plugin.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ export const boostPlugin = createBackendPlugin({
228228
path: '/health',
229229
allow: 'unauthenticated',
230230
});
231+
httpRouter.addAuthPolicy({
232+
path: '/config/status',
233+
allow: 'user-cookie',
234+
});
231235

232236
logger.info('Boost backend plugin initialized successfully');
233237
},

0 commit comments

Comments
 (0)