Skip to content

feat(#3299): runtime configuration engine with Zod validation#3521

Open
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
feat/3299-runtime-config-engine
Open

feat(#3299): runtime configuration engine with Zod validation#3521
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
feat/3299-runtime-config-engine

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Implements the runtime configuration engine for boost-backend:

  • RuntimeConfigResolver: two-layer config resolution (DB override →
    YAML baseline) with cacheService (30s TTL, immediate invalidation
    on write). Single cache layer, no duplicate wrappers.

  • AdminConfigService: DB-backed config overrides using the
    boost_admin_config table. Validates all writes against Zod schemas
    and enforces configScope (yaml-only fields rejected for DB writes).

  • Zod schemas as single source of truth: all 15 admin-configurable
    fields defined with schema, configScope annotation (yaml-only,
    db-overridable, db-only), and descriptions. config.d.ts generated
    from the same schema definitions.

  • Credential encryption: AES-256-GCM encryption for sensitive
    DB-stored values (e.g., DevSpaces credentials) with configurable
    encryption secret.

  • Schema version tracking: stores schema version alongside DB values.
    On startup, re-validates all stored values against current schemas
    and removes invalid overrides (restoring YAML baseline).

  • Plugin wired with coreServices.cache and coreServices.database
    dependencies, satisfying the cache-from-day-one architecture rule.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com


Closes #3299

Post-script verification

  • Branch is not main/master (feat/3299-runtime-config-engine)
  • Secret scan passed (gitleaks — 2e647e201781a7b120cb2e71fab2e8740be418e9..HEAD)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

Implements the runtime configuration engine for boost-backend:

- RuntimeConfigResolver: two-layer config resolution (DB override →
  YAML baseline) with cacheService (30s TTL, immediate invalidation
  on write). Single cache layer, no duplicate wrappers.

- AdminConfigService: DB-backed config overrides using the
  boost_admin_config table. Validates all writes against Zod schemas
  and enforces configScope (yaml-only fields rejected for DB writes).

- Zod schemas as single source of truth: all 15 admin-configurable
  fields defined with schema, configScope annotation (yaml-only,
  db-overridable, db-only), and descriptions. config.d.ts generated
  from the same schema definitions.

- Credential encryption: AES-256-GCM encryption for sensitive
  DB-stored values (e.g., DevSpaces credentials) with configurable
  encryption secret.

- Schema version tracking: stores schema version alongside DB values.
  On startup, re-validates all stored values against current schemas
  and removes invalid overrides (restoring YAML baseline).

- Plugin wired with coreServices.cache and coreServices.database
  dependencies, satisfying the cache-from-day-one architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@fullsend-ai-coder fullsend-ai-coder Bot requested review from a team, durandom and gabemontero as code owners June 22, 2026 03:28
@rhdh-gh-app

rhdh-gh-app Bot commented Jun 22, 2026

Copy link
Copy Markdown

Missing Changesets

The following package(s) are changed by this PR but do not have a changeset:

  • @red-hat-developer-hub/backstage-plugin-boost-backend

See CONTRIBUTING.md for more information about how to add changesets.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-boost-backend workspaces/boost/plugins/boost-backend none v0.1.1

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.01923% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.71%. Comparing base (2e647e2) to head (174a46e).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3521      +/-   ##
==========================================
+ Coverage   53.63%   53.71%   +0.08%     
==========================================
  Files        2260     2264       +4     
  Lines       85972    86180     +208     
  Branches    24186    24219      +33     
==========================================
+ Hits        46112    46293     +181     
- Misses      38302    38329      +27     
  Partials     1558     1558              
Flag Coverage Δ *Carryforward flag
adoption-insights 83.70% <ø> (ø) Carriedforward from 2e647e2
ai-integrations 67.95% <ø> (ø) Carriedforward from 2e647e2
app-defaults 69.79% <ø> (ø) Carriedforward from 2e647e2
augment 46.39% <ø> (ø) Carriedforward from 2e647e2
boost 82.00% <87.01%> (+7.35%) ⬆️
bulk-import 72.46% <ø> (ø) Carriedforward from 2e647e2
cost-management 14.10% <ø> (ø) Carriedforward from 2e647e2
dcm 61.79% <ø> (ø) Carriedforward from 2e647e2
extensions 61.53% <ø> (ø) Carriedforward from 2e647e2
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 2e647e2
global-header 59.71% <ø> (ø) Carriedforward from 2e647e2
homepage 49.84% <ø> (ø) Carriedforward from 2e647e2
install-dynamic-plugins 56.23% <ø> (ø) Carriedforward from 2e647e2
konflux 91.49% <ø> (ø) Carriedforward from 2e647e2
lightspeed 68.57% <ø> (ø) Carriedforward from 2e647e2
mcp-integrations 85.46% <ø> (ø) Carriedforward from 2e647e2
orchestrator 37.75% <ø> (ø) Carriedforward from 2e647e2
quickstart 63.76% <ø> (ø) Carriedforward from 2e647e2
sandbox 79.56% <ø> (ø) Carriedforward from 2e647e2
scorecard 83.96% <ø> (ø) Carriedforward from 2e647e2
theme 61.26% <ø> (ø) Carriedforward from 2e647e2
translations 7.25% <ø> (ø) Carriedforward from 2e647e2
x2a 78.68% <ø> (ø) Carriedforward from 2e647e2

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2e647e2...174a46e. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sonarqubecloud

Copy link
Copy Markdown

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:30 AM UTC · Completed 3:48 AM UTC
Commit: 2e647e2 · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/plugin.ts:184 — The /config/status endpoint exposes all resolved configuration values including decrypted sensitive fields (e.g., boost.devSpaces.credentials) without any authorization check. Any authenticated Backstage user (not just admins) can retrieve decrypted credentials via GET /api/boost/config/status. No httpAuth.credentials() call or permission check is performed, and no addAuthPolicy is registered for this path.
    Remediation: (1) Add httpAuth.credentials(req) to enforce authentication. (2) Add a permission check (e.g., boost.config.read) to restrict to admin users. (3) Redact sensitive field values — never return decrypted credentials over the wire.

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:148resolveAll() returns all config values including decrypted sensitive credentials. These are then serialized to the Backstage cache in plaintext via cache.set('effective-config', cacheObj). Sensitive values should not be bulk-resolved or cached in cleartext.
    Remediation: Exclude sensitive fields from resolveAll() and the cache entirely, or keep them encrypted/redacted in the cache and only decrypt on authorized single-key retrieval.

  • [logic-error] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:148 — Race condition (TOCTOU) in setOverride. Between the SELECT (.where({ key }).first()) and the subsequent UPDATE or INSERT, a concurrent request could insert a row with the same key, causing a primary-key collision, or a concurrent DELETE could cause the UPDATE to silently affect zero rows.
    Remediation: Replace the manual check-then-insert/update with knex(TABLE_NAME).insert({...}).onConflict('key').merge() for an atomic upsert, or wrap in knex.transaction().

  • [fail-open] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:70 — In getOverride and getAllOverrides, when encryptionSecret is undefined but a sensitive field contains encrypted ciphertext, the method silently returns the raw base64 ciphertext to the caller. An operator who removes the encryption secret from config will unknowingly serve garbage data to downstream consumers without any error signal.
    Remediation: When isSensitiveField(key) is true and this.encryptionSecret is undefined, throw an error or return undefined with a logged warning.

Medium

  • [config-gap] workspaces/boost/plugins/boost-backend/src/plugin.ts:126boost.encryptionSecret is read from config via getOptionalString() but is not declared in config.d.ts or boostConfigFields. Without the @visibility secret annotation, Backstage may expose this value to the frontend bundle, leaking the encryption key.

  • [logic-error] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:171 — The DB override layer in getEffectiveConfig applies all overrides without checking configScope. If a yaml-only key is inserted directly into the database, RuntimeConfigResolver would honor it, defeating the yaml-only scope guarantee.

  • [weak-crypto] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:34deriveKey() uses unsalted SHA-256 to derive the AES-256 key. SHA-256 is not a key derivation function and provides no defense against brute-force on low-entropy secrets.

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/plugin.ts:196 — The /config/status error handler returns raw String(error) in the JSON response, potentially leaking internal details such as DB connection strings or stack traces.

  • [design-direction] workspaces/boost/plugins/boost-backend/config.d.ts — The file header claims "Generated from Zod schemas" but no generation script or build step exists. Without a generation mechanism, config.d.ts can drift from the Zod schemas silently.

  • [pattern-inconsistency] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:52deriveKey uses require('crypto') while other crypto imports use ES import. The codebase exclusively uses ES imports.

  • [error-handling-idiom] workspaces/boost/plugins/boost-backend/src/plugin.ts:193 — The /config/status error handler uses raw res.status(500).json() instead of forwarding via next(error), inconsistent with the existing codebase error-handling pattern.

  • [test-inadequate] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts — No test covers the upsert-update path of setOverride (calling it twice for the same key with different values).

  • [logic-error] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:114readYamlValue speculatively calls getOptionalString/getOptionalNumber which throw on type mismatch. The outer try/catch silently swallows these errors, masking config problems.

  • [missing-doc] workspaces/boost/README.md — Plugins table says "coming soon" and directory structure says "not yet created" despite plugins existing.

  • [missing-doc] workspaces/boost/plugins/boost-backend/README.md — No README.md exists for the boost-backend plugin. Other backend plugins in the repo have READMEs.

Low / Info

  • [scope-creep] workspaces/boost/plugins/boost-backend/src/plugin.ts — Task 1.2 (cache infrastructure) from task group 1 included in this task group 2 PR. Pragmatic but crosses task group authorization.
  • [naming-convention] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:43AdminConfigRow uses snake_case properties while the codebase uses camelCase for TS interfaces.
  • [pattern-inconsistency] workspaces/boost/plugins/boost-backend/src/config/schemas.ts:106 — Unnecessary cast in isSensitiveField; isDbWritable accesses .configScope without a cast.
  • [edge-case] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:79decryptValue does not validate input length; short/empty input produces cryptic errors.
  • [missing-doc] workspaces/boost/README.md:38 — "pre-implementation" claim is outdated.
  • [api-shape] workspaces/boost/plugins/boost-backend/src/index.ts:45encryptValue/decryptValue exported as @public API; these low-level crypto primitives increase misuse risk.
  • [sub-agent-failure] N/A — Challenger sub-agent verified findings against main branch instead of PR diff. Pre-challenger finding set used as fallback.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

@@ -151,6 +184,21 @@ export const boostPlugin = createBackendPlugin({
res.json({ status: 'ok' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] data-exposure

The /config/status endpoint exposes all resolved config values including decrypted sensitive fields (boost.devSpaces.credentials) without authorization checks. Any authenticated user can retrieve decrypted credentials.

Suggested fix: Add httpAuth.credentials(req) + permission check + redact sensitive field values in response.

// Layer 1: YAML baseline
for (const key of Object.keys(boostConfigFields) as BoostConfigKey[]) {
const yamlValue = this.readYamlValue(key);
if (yamlValue !== undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] data-exposure

resolveAll() returns decrypted sensitive credentials and caches them in plaintext via cache.set('effective-config', cacheObj). Sensitive values should not be bulk-resolved or cached in cleartext.

Suggested fix: Exclude sensitive fields from resolveAll() and the cache, or keep encrypted/redacted in cache.

const knex = await this.getDb();
const rows = await knex<AdminConfigRow>(TABLE_NAME).select();
const result = new Map<string, unknown>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] logic-error

TOCTOU race condition in setOverride upsert. Between SELECT and INSERT/UPDATE, concurrent requests can cause PK collisions or lost updates.

Suggested fix: Use knex(TABLE_NAME).insert({...}).onConflict('key').merge() or wrap in knex.transaction().

private readonly logger: LoggerService;
private readonly encryptionSecret?: string;
private knexPromise: Promise<Knex> | undefined;
private readonly database: DatabaseService;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] fail-open

getOverride/getAllOverrides silently return raw ciphertext when encryptionSecret is missing, serving garbage data to downstream consumers without error signal.

Suggested fix: When isSensitiveField(key) is true and encryptionSecret is undefined, throw error or return undefined with logged warning.

@@ -120,6 +126,33 @@ export const boostPlugin = createBackendPlugin({
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] config-gap

boost.encryptionSecret not declared in config.d.ts with @visibility secret. Without this, Backstage may expose the encryption key to the frontend.

@@ -0,0 +1,139 @@
/*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] design-direction

config.d.ts claims to be generated from Zod schemas but no generation script exists. Can drift from Zod schemas silently.

const cipher = createCipheriv(ALGORITHM, key, iv, {
authTagLength: AUTH_TAG_LENGTH,
});
const encrypted = Buffer.concat([

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] pattern-inconsistency

deriveKey uses require('crypto') while other imports in the same file use ES import syntax. Codebase exclusively uses ES imports.

const allConfig = await runtimeConfigResolver.resolveAll();
const configEntries = Object.fromEntries(allConfig);
res.json({
status: 'ok',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] error-handling-idiom

The /config/status error handler uses raw res.status(500).json() instead of next(error), inconsistent with existing codebase error-handling patterns.

@@ -0,0 +1,277 @@
/*

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] test-inadequate

No test covers the upsert-update path of setOverride (calling it twice for the same key with different values).

*
* @param key - The config field key.
* @param value - The value to store.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[medium] logic-error

readYamlValue speculatively calls getOptionalString/getOptionalNumber which throw on type mismatch. Outer try/catch silently swallows errors, masking config problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

boost-backend — Runtime configuration engine with Zod validation (issue 3 of 15)

0 participants