fix(outbox): gate PG OutboxAutoConfiguration on backend=PG (resolves Phase 0a B2) #9

Merged
hibryda merged 3 commits from feat/outbox-pg-backend-gate into main 2026-05-28 15:43:20 +02:00
Owner

Resolves the Phase 0a B2 deferred item (now load-bearing for Wave A.2 Phase 1)

The PG OutboxAutoConfiguration activated on im2be.outbox.enabled=true alone. A Redis-only consumer (identity-service: im2be.outbox.backend=REDIS) that sets enabled=true therefore ALSO triggered the PG graph — which wires JPA + a ShedLock JdbcTemplateLockProvider needing a DataSource + spring-jdbc it doesn't have. Condition evaluation crashes at context load:

Error processing condition on com.aim2be.platform.outbox.OutboxAutoConfiguration.outboxLockProvider
Caused by: NoClassDefFound ... JdbcTemplateLockProvider

This is exactly the B2 item Phase 0a deferred "to when the Redis module lands" — it just bit identity-service's Phase 1a wiring.

Fix

Gate the whole PG autoconfig:

@ConditionalOnExpression(
    "'${im2be.outbox.enabled:false}' == 'true' and '${im2be.outbox.backend:PG}' == 'PG'")
  • backend defaults to PG → the 7 PG services (enabled=true, no explicit backend) are unchanged.
  • backend=REDIS → the entire PG graph is skipped; RedisOutboxAutoConfiguration (Phase 0b) supplies the OutboxBackend bean instead.

Tests

New OutboxBackendSelectionTest (4, ApplicationContextRunner): PG-active for enabled+default-backend AND enabled+backend=PG (mock DataSource/Kafka/repo so the active graph wires); skipped for backend=REDIS and enabled=false. Catches an expression typo that would otherwise silently disable all 7 PG services.

Verification

  • mvn -pl outbox-publisher -am clean verify → BUILD SUCCESS, 0 [WARNING], 100 unit (+4) + 4 IT green.

Note: the Forgejo mvn install CI check is red on this repo for all commits (pre-existing runner-toolchain bug, PR-PLATFORM-CI-1). Verified locally.

## Resolves the Phase 0a B2 deferred item (now load-bearing for Wave A.2 Phase 1) The PG `OutboxAutoConfiguration` activated on `im2be.outbox.enabled=true` alone. A **Redis-only** consumer (identity-service: `im2be.outbox.backend=REDIS`) that sets `enabled=true` therefore ALSO triggered the PG graph — which wires JPA + a ShedLock `JdbcTemplateLockProvider` needing a `DataSource` + spring-jdbc it doesn't have. Condition evaluation crashes at context load: ``` Error processing condition on com.aim2be.platform.outbox.OutboxAutoConfiguration.outboxLockProvider Caused by: NoClassDefFound ... JdbcTemplateLockProvider ``` This is exactly the **B2** item Phase 0a deferred "to when the Redis module lands" — it just bit identity-service's Phase 1a wiring. ### Fix Gate the whole PG autoconfig: ```java @ConditionalOnExpression( "'${im2be.outbox.enabled:false}' == 'true' and '${im2be.outbox.backend:PG}' == 'PG'") ``` - `backend` defaults to `PG` → the **7 PG services** (enabled=true, no explicit backend) are **unchanged**. - `backend=REDIS` → the entire PG graph is skipped; `RedisOutboxAutoConfiguration` (Phase 0b) supplies the `OutboxBackend` bean instead. ### Tests New `OutboxBackendSelectionTest` (4, ApplicationContextRunner): PG-active for enabled+default-backend AND enabled+backend=PG (mock DataSource/Kafka/repo so the active graph wires); skipped for `backend=REDIS` and `enabled=false`. Catches an expression typo that would otherwise silently disable all 7 PG services. ### Verification - `mvn -pl outbox-publisher -am clean verify` → BUILD SUCCESS, **0 `[WARNING]`**, 100 unit (+4) + 4 IT green. Note: the Forgejo `mvn install` CI check is red on this repo for all commits (pre-existing runner-toolchain bug, PR-PLATFORM-CI-1). Verified locally.
fix(outbox): gate PG OutboxAutoConfiguration on backend=PG (resolves Phase 0a B2)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 3s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
db7788dbce
The PG OutboxAutoConfiguration activated on im2be.outbox.enabled=true alone, so a
Redis-only consumer (identity-service: im2be.outbox.backend=REDIS) that sets
enabled=true ALSO triggered the PG graph — which wires JPA + a ShedLock
JdbcTemplateLockProvider needing a DataSource + spring-jdbc the consumer does not
have. Condition evaluation then crashes at context load ("Error processing
condition on OutboxAutoConfiguration.outboxLockProvider"). This is the Phase 0a
B2 deferred item, now load-bearing because the Phase 0b Redis backend + its first
consumer (identity Phase 1a) exist.

Fix: gate the whole PG autoconfig with
@ConditionalOnExpression("'${im2be.outbox.enabled:false}' == 'true' and
'${im2be.outbox.backend:PG}' == 'PG'"). The backend default is PG, so the 7 PG
services (enabled=true, no explicit backend) are UNCHANGED; a Redis consumer
(backend=REDIS) skips the entire PG graph, and RedisOutboxAutoConfiguration
(Phase 0b) supplies the OutboxBackend bean instead. Class Javadoc documents the
gate + why a Redis-only classpath cannot load the PG graph.

New OutboxBackendSelectionTest (4 cases, ApplicationContextRunner): PG-active for
enabled+default-backend AND enabled+backend=PG (supplying mock DataSource/Kafka/
repo so the active graph wires); skipped for backend=REDIS and for enabled=false.
Catches an expression typo that would silently disable all 7 PG services.

Verification:
- mvn -pl outbox-publisher -am clean verify -> BUILD SUCCESS, 0 [WARNING]
- 100 unit (+4 new) + 4 IT green; the 7-PG-service path unchanged (default PG).

Superseded by round 2.

Show previous round

hib-pr-reviewer review — PR #9 (affinity-intelligence-rework/im2be-platform-libs)

Round 1 — head db7788dbcee7, base main, trigger opened

TL;DR: CONDITIONAL_APPROVE — kept 1 agreed minor finding (SpEL case-sensitivity regression) and 2 verified unique info findings (missing no-DataSource test scenario; misleading Javadoc); no blocking issues.

Summary

Reconciliation: 3 findings kept (1 agreed minor + 2 unique info, all verified)

Agreed finding kept (1): Both A and B independently flag the SpEL == case-sensitivity regression on line 118. Reading the file confirms the exact expression '${im2be.outbox.enabled:false}' == 'true' and '${im2be.outbox.backend:PG}' == 'PG'. I also verified RedisOutboxAutoConfiguration.java line 98 uses @ConditionalOnProperty(havingValue = "REDIS") — which IS case-insensitive — confirming the asymmetry A described (A incorrectly claimed it was "line 98 of that file" rather than a sibling module, but the substance is correct). Finding kept at minor severity.

Unique-to-A (info, verified): A claims pgAutoConfigSkippedWhenBackendRedis exercises the gate via a shared runner that already includes a DataSource mock, and therefore does not cover the production crash scenario (no DataSource + backend=REDIS). Reading lines 27–33 of OutboxBackendSelectionTest.java confirms .withBean(DataSource.class, () -> mock(DataSource.class)) is part of the class-level runner used by all four tests including the REDIS case. The missing-DataSource scenario is the exact original failure mode the gate was designed to prevent. Finding kept at info severity.

Unique-to-B (info, verified): B flags that the class-level Javadoc (lines 19–22) and the per-test comment (lines 58–61) both assert "classpath has no DataSource / spring-jdbc" yet the shared runner provides one. Reading the test file confirms the Javadoc text at line 19 ("whose classpath has no DataSource / spring-jdbc") is factually false given the runner definition. This is a distinct, actionable documentation fix separate from A's test-gap concern. Finding kept at info severity.

No findings dropped. Total reconciled: 3 (1 minor, 2 info).

Blast Radius

The diff modifies the class-level activation condition of OutboxAutoConfiguration, which is the PG outbox auto-configuration consumed by 7 services. A misconfigured condition (e.g. the case-sensitivity regression) would silently deactivate the entire PG outbox graph in all 7 consumers with no error at startup. The Redis module and its consumers are unaffected by this PR directly, but the asymmetry between the two gates now creates a consistency risk for future maintainers.

BLAST_SCORE: 5/10

CI status (head db7788dbcee7)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (3)

[MINOR] SpEL == is case-sensitive — silent regression from @ConditionalOnProperty's equalsIgnoreCase for both enabled and backend

outbox-publisher/src/main/java/com/aim2be/platform/outbox/OutboxAutoConfiguration.java:118

Line 117–118:

@ConditionalOnExpression(
    "'${im2be.outbox.enabled:false}' == 'true' and '${im2be.outbox.backend:PG}' == 'PG'")

SpEL == delegates to String.equals() — strict case comparison. The replaced @ConditionalOnProperty(havingValue = "true") used equalsIgnoreCase internally, so enabled=TRUE or enabled=True (both legal in YAML and common in environment-variable injection) activated the prior config. Under the new expression those values silently evaluate to false and the entire PG graph is skipped with no diagnostic.

The backend comparison carries a separate risk: Spring Boot's relaxed binding normalises pgPG when binding to OutboxProperties.Backend at runtime, but that normalisation does not apply inside @ConditionalOnExpression placeholder resolution. A YAML author writing im2be.outbox.backend: pg (lower-case, idiomatic YAML) gets 'pg' == 'PG'false, the PG graph is skipped, no Redis graph exists, and the context loads with no OutboxBackend bean and no meaningful error.

Asymmetry confirmed: RedisOutboxAutoConfiguration (redis-outbox-backend module, line 98) still uses @ConditionalOnProperty(prefix = "im2be.outbox", name = "backend", havingValue = "REDIS") — the case-insensitive path — so the two halves of the gate are now inconsistent.

Fix — use .equalsIgnoreCase() in SpEL:

@ConditionalOnExpression(
    "'${im2be.outbox.enabled:false}'.equalsIgnoreCase('true') "
    + "and '${im2be.outbox.backend:PG}'.equalsIgnoreCase('PG')")

Alternatively, keep the battle-tested @ConditionalOnProperty for the boolean and add a second property condition for backend:

@ConditionalOnProperty(prefix = "im2be.outbox", name = "enabled", havingValue = "true")
@ConditionalOnProperty(prefix = "im2be.outbox", name = "backend", havingValue = "PG", matchIfMissing = true)

(Two @ConditionalOnProperty annotations are ANDed by Spring.)

[INFO] pgAutoConfigSkippedWhenBackendRedis does not exercise the original crash path — shared runner provides a DataSource mock

outbox-publisher/src/test/java/com/aim2be/platform/outbox/OutboxBackendSelectionTest.java:57

The class-level runner (lines 27–33) registers .withBean(DataSource.class, () -> mock(DataSource.class)) and is reused by all four tests, including pgAutoConfigSkippedWhenBackendRedis at line 57. The bug that prompted this PR was a context crash on JdbcTemplateLockProvider condition evaluation when no DataSource is present (identity-service, Redis-only classpath). The existing test proves the gate fires and suppresses OutboxBackend/OutboxPublisher beans, but does not prove the gate fires when DataSource is absent from the context — which is the production scenario.

Consider adding a dedicated test without the DataSource bean to guard against regression of the original crash independently of the mock infrastructure:

@Test
void pgAutoConfigSkippedWhenBackendRedis_noDataSource() {
    new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(OutboxAutoConfiguration.class))
        .withBean(KafkaTemplate.class, () -> mock(KafkaTemplate.class))
        // deliberately no DataSource — mirrors the identity-service Redis-only classpath
        .withPropertyValues("im2be.outbox.enabled=true", "im2be.outbox.backend=REDIS")
        .run(ctx -> assertThat(ctx).hasNotFailed());
}

[INFO] Class Javadoc (and per-test comment) falsely claim no DataSource is present for the REDIS test case

outbox-publisher/src/test/java/com/aim2be/platform/outbox/OutboxBackendSelectionTest.java:19

Lines 19–22 of the class Javadoc read:

"whose classpath has no DataSource / spring-jdbc — otherwise condition evaluation on the ShedLock JdbcTemplateLockProvider bean crashes at context load"

Lines 58–61 of pgAutoConfigSkippedWhenBackendRedis similarly say "the absent JdbcTemplateLockProvider".

Both claims are contradicted by the shared runner definition (lines 27–33), which registers .withBean(DataSource.class, () -> mock(DataSource.class)) for all four tests including the REDIS case. The test is not simulating a no-DataSource classpath; it is verifying that the gate's CLOSED decision prevents OutboxBackend and OutboxPublisher beans from appearing (a valid and correct assertion). The comments should be updated to reflect what the test actually exercises, for example:

  • Class Javadoc: "verifies the gate skips the entire PG graph when backend=REDIS is set, regardless of whether a DataSource is present"
  • Per-test comment: "The B2 fix: the PG gate must be CLOSED for a Redis-only consumer; no PG beans should appear even when a DataSource is in context"

Note: the actual crash-regression scenario (no DataSource + backend=REDIS) is the subject of the separate finding at line 57.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 1 • 3 findings (1m/2i) • 2026-05-28T13:24:05.739Z → 2026-05-28T13:26:22.379Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 2._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #9 (affinity-intelligence-rework/im2be-platform-libs) **Round 1** — head `db7788dbcee7`, base `main`, trigger `opened` **TL;DR:** CONDITIONAL_APPROVE — kept 1 agreed minor finding (SpEL case-sensitivity regression) and 2 verified unique info findings (missing no-DataSource test scenario; misleading Javadoc); no blocking issues. ### Summary ## Reconciliation: 3 findings kept (1 agreed minor + 2 unique info, all verified) **Agreed finding kept (1):** Both A and B independently flag the SpEL `==` case-sensitivity regression on line 118. Reading the file confirms the exact expression `'${im2be.outbox.enabled:false}' == 'true' and '${im2be.outbox.backend:PG}' == 'PG'`. I also verified `RedisOutboxAutoConfiguration.java` line 98 uses `@ConditionalOnProperty(havingValue = "REDIS")` — which IS case-insensitive — confirming the asymmetry A described (A incorrectly claimed it was "line 98 of that file" rather than a sibling module, but the substance is correct). Finding kept at minor severity. **Unique-to-A (info, verified):** A claims `pgAutoConfigSkippedWhenBackendRedis` exercises the gate via a shared runner that already includes a `DataSource` mock, and therefore does not cover the production crash scenario (no DataSource + backend=REDIS). Reading lines 27–33 of `OutboxBackendSelectionTest.java` confirms `.withBean(DataSource.class, () -> mock(DataSource.class))` is part of the class-level `runner` used by all four tests including the REDIS case. The missing-DataSource scenario is the exact original failure mode the gate was designed to prevent. Finding kept at info severity. **Unique-to-B (info, verified):** B flags that the class-level Javadoc (lines 19–22) and the per-test comment (lines 58–61) both assert "classpath has no DataSource / spring-jdbc" yet the shared runner provides one. Reading the test file confirms the Javadoc text at line 19 ("whose classpath has no DataSource / spring-jdbc") is factually false given the runner definition. This is a distinct, actionable documentation fix separate from A's test-gap concern. Finding kept at info severity. No findings dropped. Total reconciled: 3 (1 minor, 2 info). ### Blast Radius The diff modifies the class-level activation condition of `OutboxAutoConfiguration`, which is the PG outbox auto-configuration consumed by 7 services. A misconfigured condition (e.g. the case-sensitivity regression) would silently deactivate the entire PG outbox graph in all 7 consumers with no error at startup. The Redis module and its consumers are unaffected by this PR directly, but the asymmetry between the two gates now creates a consistency risk for future maintainers. **BLAST_SCORE: 5/10** ### CI status (head `db7788dbcee7`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/71/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/71/jobs/1) | ### Findings (3) #### **[MINOR]** SpEL `==` is case-sensitive — silent regression from `@ConditionalOnProperty`'s `equalsIgnoreCase` for both `enabled` and `backend` _outbox-publisher/src/main/java/com/aim2be/platform/outbox/OutboxAutoConfiguration.java:118_ Line 117–118: ```java @ConditionalOnExpression( "'${im2be.outbox.enabled:false}' == 'true' and '${im2be.outbox.backend:PG}' == 'PG'") ``` SpEL `==` delegates to `String.equals()` — strict case comparison. The replaced `@ConditionalOnProperty(havingValue = "true")` used `equalsIgnoreCase` internally, so `enabled=TRUE` or `enabled=True` (both legal in YAML and common in environment-variable injection) activated the prior config. Under the new expression those values silently evaluate to `false` and the entire PG graph is skipped with no diagnostic. The `backend` comparison carries a separate risk: Spring Boot's relaxed binding normalises `pg` → `PG` when binding to `OutboxProperties.Backend` at runtime, but that normalisation does **not** apply inside `@ConditionalOnExpression` placeholder resolution. A YAML author writing `im2be.outbox.backend: pg` (lower-case, idiomatic YAML) gets `'pg' == 'PG'` → `false`, the PG graph is skipped, no Redis graph exists, and the context loads with no `OutboxBackend` bean and no meaningful error. Asymmetry confirmed: `RedisOutboxAutoConfiguration` (redis-outbox-backend module, line 98) still uses `@ConditionalOnProperty(prefix = "im2be.outbox", name = "backend", havingValue = "REDIS")` — the case-insensitive path — so the two halves of the gate are now inconsistent. **Fix** — use `.equalsIgnoreCase()` in SpEL: ```java @ConditionalOnExpression( "'${im2be.outbox.enabled:false}'.equalsIgnoreCase('true') " + "and '${im2be.outbox.backend:PG}'.equalsIgnoreCase('PG')") ``` Alternatively, keep the battle-tested `@ConditionalOnProperty` for the boolean and add a second property condition for backend: ```java @ConditionalOnProperty(prefix = "im2be.outbox", name = "enabled", havingValue = "true") @ConditionalOnProperty(prefix = "im2be.outbox", name = "backend", havingValue = "PG", matchIfMissing = true) ``` (Two `@ConditionalOnProperty` annotations are ANDed by Spring.) #### **[INFO]** `pgAutoConfigSkippedWhenBackendRedis` does not exercise the original crash path — shared runner provides a `DataSource` mock _outbox-publisher/src/test/java/com/aim2be/platform/outbox/OutboxBackendSelectionTest.java:57_ The class-level runner (lines 27–33) registers `.withBean(DataSource.class, () -> mock(DataSource.class))` and is reused by all four tests, including `pgAutoConfigSkippedWhenBackendRedis` at line 57. The bug that prompted this PR was a context crash on `JdbcTemplateLockProvider` condition evaluation when **no DataSource is present** (identity-service, Redis-only classpath). The existing test proves the gate fires and suppresses `OutboxBackend`/`OutboxPublisher` beans, but does not prove the gate fires when DataSource is absent from the context — which is the production scenario. Consider adding a dedicated test without the `DataSource` bean to guard against regression of the original crash independently of the mock infrastructure: ```java @Test void pgAutoConfigSkippedWhenBackendRedis_noDataSource() { new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(OutboxAutoConfiguration.class)) .withBean(KafkaTemplate.class, () -> mock(KafkaTemplate.class)) // deliberately no DataSource — mirrors the identity-service Redis-only classpath .withPropertyValues("im2be.outbox.enabled=true", "im2be.outbox.backend=REDIS") .run(ctx -> assertThat(ctx).hasNotFailed()); } ``` #### **[INFO]** Class Javadoc (and per-test comment) falsely claim no `DataSource` is present for the REDIS test case _outbox-publisher/src/test/java/com/aim2be/platform/outbox/OutboxBackendSelectionTest.java:19_ Lines 19–22 of the class Javadoc read: > "whose classpath has no `DataSource` / spring-jdbc — otherwise condition evaluation on the ShedLock `JdbcTemplateLockProvider` bean crashes at context load" Lines 58–61 of `pgAutoConfigSkippedWhenBackendRedis` similarly say "the absent `JdbcTemplateLockProvider`". Both claims are contradicted by the shared `runner` definition (lines 27–33), which registers `.withBean(DataSource.class, () -> mock(DataSource.class))` for all four tests including the REDIS case. The test is not simulating a no-DataSource classpath; it is verifying that the gate's CLOSED decision prevents `OutboxBackend` and `OutboxPublisher` beans from appearing (a valid and correct assertion). The comments should be updated to reflect what the test actually exercises, for example: - Class Javadoc: _"verifies the gate skips the entire PG graph when `backend=REDIS` is set, regardless of whether a `DataSource` is present"_ - Per-test comment: _"The B2 fix: the PG gate must be CLOSED for a Redis-only consumer; no PG beans should appear even when a DataSource is in context"_ Note: the actual crash-regression scenario (no DataSource + backend=REDIS) is the subject of the separate finding at line 57. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 1 • 3 findings (1m/2i) • 2026-05-28T13:24:05.739Z → 2026-05-28T13:26:22.379Z • posted-as: pr-reviewer-bot</sub> </details>
fix(outbox): apply PR #9 R1 reviewer findings (B2 gate)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 2s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
7bdf98e641
R1 verdict CONDITIONAL_APPROVE, findings (kept=3: 1 MINOR / 2 INFO):

(1) MINOR — SpEL '==' is case-SENSITIVE, a regression from the replaced
    @ConditionalOnProperty(havingValue) which matches case-INSENSITIVELY.
    enabled=TRUE / backend: pg (idiomatic YAML + env-var casing) would have
    silently skipped the PG graph with no diagnostic; also inconsistent with the
    Redis module's case-insensitive @ConditionalOnProperty(havingValue=REDIS).
(2) INFO — OutboxBackendSelectionTest's REDIS case used the shared runner that
    provides a mock DataSource, so it never exercised the ACTUAL crash path
    (no DataSource present — identity's Redis-only classpath).
(3) INFO — the class Javadoc + per-test comment claimed "no DataSource" while
    the shared runner supplies one (contradiction).

Fix:
- (1) gate now uses .equalsIgnoreCase('true') / .equalsIgnoreCase('PG') so
  enabled=TRUE and backend=pg still match (parity with @ConditionalOnProperty +
  Spring relaxed binding). Class Javadoc documents the case-insensitivity + why
  a SpEL == would regress.
- (2) new pgAutoConfigSkippedWhenBackendRedisNoDataSourceDoesNotCrash — a FRESH
  runner with NO DataSource bean (mirrors identity's Redis-only classpath),
  asserting the context loads cleanly (the gate prevents the original
  JdbcTemplateLockProvider crash). This is the real regression guard.
- (1-guard) new gateIsCaseInsensitiveForEnabledAndBackend (enabled=TRUE +
  backend=pg → PG graph active) + gateSkipsForLowercaseRedisBackend
  (backend=redis → skipped) — would fail under a SpEL == regression.
- (3) class Javadoc + per-test comment corrected: the shared runner supplies
  mocks to prove bean-gating + active-graph wiring; the dedicated no-DataSource
  test proves crash-prevention.

Verification:
- mvn -pl outbox-publisher -am clean verify -> BUILD SUCCESS, 0 [WARNING]
- 103 unit (+3) + 4 IT green.

Superseded by round 3.

Show previous round

hib-pr-reviewer review — PR #9 (affinity-intelligence-rework/im2be-platform-libs)

Round 2 — head 7bdf98e64173, base main, trigger synchronize

TL;DR: CONDITIONAL_APPROVE — all three round-1 findings confirmed closed by both reviewers; one unique-to-B minor finding (SpEL injection via ${…} interpolation at line 123) verified against source and kept.

Summary

Arbitration — Round 2

Round-1 findings (R1-F1 SpEL case sensitivity, R1-F2 crash-path coverage, R1-F3 false Javadoc): Both reviewers agree all three are closed. No arbitration needed.

Unique-to-B finding (SpEL injection via ${…} interpolation): Verified by reading lines 122-124 of OutboxAutoConfiguration.java. The code exactly matches what Reviewer B described:

@ConditionalOnExpression(
        "'${im2be.outbox.enabled:false}'.equalsIgnoreCase('true') "
                + "and '${im2be.outbox.backend:PG}'.equalsIgnoreCase('PG')")

Spring resolves ${im2be.outbox.backend:PG} to the raw property string before handing the concatenated expression to the SpEL parser. A value containing a single-quote (e.g. PG') produces a broken string literal; a crafted value can escape the literal and execute arbitrary SpEL. The injection surface is genuine. Severity minor is correct — these are developer/ops-controlled properties in practice, so exploitation requires compromising the config pipeline, but the antipattern is real and the fix is low-effort. Finding kept.

Blast Radius

The diff touches two files: one auto-configuration class and one new test class. The @ConditionalOnExpression change gates a single auto-configuration bean graph and affects only services that set im2be.outbox.backend; the seven existing PG services are unaffected by default. Blast radius is contained to the outbox-publisher module.

BLAST_SCORE: 3/10

Risk Indicators

Indicator Value
Sensitive functions @ConditionalOnExpression
Migration touched
Test delta
Dependency changes

CI status (head 7bdf98e64173)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings (1)

[MINOR] SpEL injection surface: ${...} property values embedded as string literals in @ConditionalOnExpression

outbox-publisher/src/main/java/com/aim2be/platform/outbox/OutboxAutoConfiguration.java:123

Lines 123-124 embed property values directly into the SpEL expression string via ${…} interpolation:

"'${im2be.outbox.enabled:false}'.equalsIgnoreCase('true') "
        + "and '${im2be.outbox.backend:PG}'.equalsIgnoreCase('PG')"

Spring resolves ${im2be.outbox.backend:PG} to the raw property value and concatenates it into the expression string before SpEL parsing. A value containing a single-quote breaks the SpEL parser; a crafted value such as PG'.concat(T(java.lang.Runtime).getRuntime().exec('...')).' can escape the string literal and execute arbitrary SpEL at context load time. In typical deployments these properties are developer/ops-controlled, so exploitation requires compromising the config pipeline (external config server or env-var injection), but the pattern is a recognised Spring security antipattern and the fix is a one-liner.

Recommended fix — resolve property values at runtime via environment.getProperty() so they are never embedded in the expression string:

@ConditionalOnExpression(
        "environment.getProperty('im2be.outbox.enabled','false').equalsIgnoreCase('true') "
                + "and environment.getProperty('im2be.outbox.backend','PG').equalsIgnoreCase('PG')")

environment is the ConfigurableEnvironment bound in the @ConditionalOnExpression evaluation context by ConditionEvaluator. Property values are fetched as String objects at runtime — never spliced into the expression string — so the injection surface is eliminated. Relaxed binding is preserved because Spring Boot's ConfigurationPropertySources are already registered on that Environment. No test changes required; the existing six-test suite covers both open/closed semantics and case-insensitivity in both directions.

Verdict

CONDITIONAL_APPROVE


hib-pr-reviewer • round 2 • 1 finding (1m) • 2026-05-28T13:33:03.187Z → 2026-05-28T13:33:51.993Z • posted-as: pr-reviewer-bot

<!-- hib-pr-reviewer collapsed --> > _Superseded by round 3._ <details> <summary>Show previous round</summary> ## hib-pr-reviewer review — PR #9 (affinity-intelligence-rework/im2be-platform-libs) **Round 2** — head `7bdf98e64173`, base `main`, trigger `synchronize` **TL;DR:** CONDITIONAL_APPROVE — all three round-1 findings confirmed closed by both reviewers; one unique-to-B minor finding (SpEL injection via ${…} interpolation at line 123) verified against source and kept. ### Summary ## Arbitration — Round 2 **Round-1 findings (R1-F1 SpEL case sensitivity, R1-F2 crash-path coverage, R1-F3 false Javadoc):** Both reviewers agree all three are closed. No arbitration needed. **Unique-to-B finding (SpEL injection via `${…}` interpolation):** Verified by reading lines 122-124 of `OutboxAutoConfiguration.java`. The code exactly matches what Reviewer B described: ```java @ConditionalOnExpression( "'${im2be.outbox.enabled:false}'.equalsIgnoreCase('true') " + "and '${im2be.outbox.backend:PG}'.equalsIgnoreCase('PG')") ``` Spring resolves `${im2be.outbox.backend:PG}` to the raw property string before handing the concatenated expression to the SpEL parser. A value containing a single-quote (e.g. `PG'`) produces a broken string literal; a crafted value can escape the literal and execute arbitrary SpEL. The injection surface is genuine. Severity **minor** is correct — these are developer/ops-controlled properties in practice, so exploitation requires compromising the config pipeline, but the antipattern is real and the fix is low-effort. Finding kept. ### Blast Radius The diff touches two files: one auto-configuration class and one new test class. The `@ConditionalOnExpression` change gates a single auto-configuration bean graph and affects only services that set `im2be.outbox.backend`; the seven existing PG services are unaffected by default. Blast radius is contained to the outbox-publisher module. **BLAST_SCORE: 3/10** ### Risk Indicators | Indicator | Value | |---|---| | Sensitive functions | `@ConditionalOnExpression` | | Migration touched | — | | Test delta | — | | Dependency changes | — | ### CI status (head `7bdf98e64173`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/72/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/72/jobs/1) | ### Findings (1) #### **[MINOR]** SpEL injection surface: `${...}` property values embedded as string literals in `@ConditionalOnExpression` _outbox-publisher/src/main/java/com/aim2be/platform/outbox/OutboxAutoConfiguration.java:123_ Lines 123-124 embed property values directly into the SpEL expression string via `${…}` interpolation: ```java "'${im2be.outbox.enabled:false}'.equalsIgnoreCase('true') " + "and '${im2be.outbox.backend:PG}'.equalsIgnoreCase('PG')" ``` Spring resolves `${im2be.outbox.backend:PG}` to the raw property value and concatenates it into the expression string before SpEL parsing. A value containing a single-quote breaks the SpEL parser; a crafted value such as `PG'.concat(T(java.lang.Runtime).getRuntime().exec('...')).'` can escape the string literal and execute arbitrary SpEL at context load time. In typical deployments these properties are developer/ops-controlled, so exploitation requires compromising the config pipeline (external config server or env-var injection), but the pattern is a recognised Spring security antipattern and the fix is a one-liner. **Recommended fix — resolve property values at runtime via `environment.getProperty()` so they are never embedded in the expression string:** ```java @ConditionalOnExpression( "environment.getProperty('im2be.outbox.enabled','false').equalsIgnoreCase('true') " + "and environment.getProperty('im2be.outbox.backend','PG').equalsIgnoreCase('PG')") ``` `environment` is the `ConfigurableEnvironment` bound in the `@ConditionalOnExpression` evaluation context by `ConditionEvaluator`. Property values are fetched as `String` objects at runtime — never spliced into the expression string — so the injection surface is eliminated. Relaxed binding is preserved because Spring Boot's `ConfigurationPropertySources` are already registered on that `Environment`. No test changes required; the existing six-test suite covers both open/closed semantics and case-insensitivity in both directions. ### Verdict **CONDITIONAL_APPROVE** --- <sub>hib-pr-reviewer • round 2 • 1 finding (1m) • 2026-05-28T13:33:03.187Z → 2026-05-28T13:33:51.993Z • posted-as: pr-reviewer-bot</sub> </details>
fix(outbox): replace SpEL-injection-prone gate with a custom Condition (PR #9 R2)
Some checks failed
im2be-platform-libs CI / mvn install (pull_request) Failing after 3s
im2be-platform-libs CI / mvn verify (main only) (pull_request) Has been skipped
ac825147eb
R2 verdict CONDITIONAL_APPROVE; all 3 R1 findings confirmed closed. 1 new MINOR
(kept):

(1) MINOR — SpEL injection surface: the @ConditionalOnExpression embedded
    ${im2be.outbox.backend} / ${im2be.outbox.enabled} raw property VALUES into
    the expression string via ${…} interpolation. A crafted value (single-quote
    + T(java.lang.Runtime)…) escapes the string literal and executes arbitrary
    SpEL at context load — the recognised SpEL-injection antipattern. Internal
    config makes it low-risk, but it is a real surface (rule 1).

Fix: replaced @ConditionalOnExpression with a custom @Conditional
(OnPgOutboxBackendCondition) that reads the Environment directly off the
ConditionContext in Java + compares with equalsIgnoreCase. No SpEL, no ${}
interpolation → no injection surface; still case-insensitive (reviewer R1).

NB (rule 63 — verified the suggestion before applying): the reviewer's suggested
`environment.getProperty(...)` SpEL form does NOT work in @ConditionalOnExpression
— at condition-eval time the BeanExpressionContext exposes neither an
`environment` property (EL1008E) nor an `@environment` bean (EL1058E,
NoSuchBeanDefinition). The custom Condition is the robust path: ConditionContext
.getEnvironment() is always available. The condition's Javadoc records both the
injection rationale and the failed-SpEL-forms for the next maintainer.

Verification:
- mvn -pl outbox-publisher -am clean verify -> BUILD SUCCESS, 0 [WARNING]
- 103 unit + 4 IT green; OutboxBackendSelectionTest (7) unchanged + passing
  against the custom Condition (PG-active default/PG, skipped REDIS + disabled +
  no-DataSource crash-path, case-insensitive enabled=TRUE/backend=pg).

hib-pr-reviewer review — PR #9 (affinity-intelligence-rework/im2be-platform-libs)

Round 3 — head ac825147eb59, base main, trigger synchronize

TL;DR: NO_NEW_FINDINGS — No new findings this round.

Summary

[quorum-converged] A=0 = B=0. ## Round 3 Review

All four prior-round findings are resolved:

Round Finding Status
R1 SpEL == case-sensitivity at OutboxAutoConfiguration.java:118 Fixed — OnPgOutboxBackendCondition uses equalsIgnoreCase for both enabled and backend
R1 pgAutoConfigSkippedWhenBackendRedis doesn't exercise the crash path (shared runner has a DataSource) Fixed — dedicated pgAutoConfigSkippedWhenBackendRedisNoDataSourceDoesNotCrash test at line 82 uses a completely fresh runner with no DataSource, KafkaTemplate, or OutboxRecordRepository
R1 Class Javadoc falsely claimed no DataSource was present Fixed — Javadoc at line 21–26 now explicitly calls out that the shared runner does supply a mock DataSource
R2 SpEL injection surface via ${…} interpolation in @ConditionalOnExpression Fixed — @ConditionalOnExpression replaced entirely with @Conditional(OnPgOutboxBackendCondition.class); condition reads Environment directly in Java with no SpEL involved

Implementation quality: OnPgOutboxBackendCondition is correct and minimal. ConditionContext.getEnvironment() is guaranteed non-null by Spring's contract; the default-value overloads ensure null-safety; the class is intentionally package-private (Spring processes @Conditional via reflection so visibility is irrelevant). The @AutoConfigureAfter entries are pre-existing and Spring handles gracefully-absent ordering targets.

Test coverage: Seven complementary tests cover the open path (default, explicit PG, mixed case), the closed path (REDIS with and without DataSource, lowercase redis, disabled), and the exact crash scenario that motivated the PR. No actionable new findings.

CI status (head ac825147eb59)

Overall: ✗ failure

2 checks: 2 pending

Check State Link
im2be-platform-libs CI / mvn install (pull_request) pending details
im2be-platform-libs CI / mvn verify (main only) (pull_request) pending details

Findings

No new findings this round.

Quorum converged on empty findings (A + B both returned 0).

Verdict

NO_NEW_FINDINGS


hib-pr-reviewer • round 3 • 0 findings • 2026-05-28T13:40:04.359Z → 2026-05-28T13:42:16.004Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]

## hib-pr-reviewer review — PR #9 (affinity-intelligence-rework/im2be-platform-libs) **Round 3** — head `ac825147eb59`, base `main`, trigger `synchronize` **TL;DR:** NO_NEW_FINDINGS — No new findings this round. ### Summary [quorum-converged] A=0 = B=0. ## Round 3 Review All four prior-round findings are resolved: | Round | Finding | Status | |---|---|---| | R1 | SpEL `==` case-sensitivity at `OutboxAutoConfiguration.java:118` | ✅ Fixed — `OnPgOutboxBackendCondition` uses `equalsIgnoreCase` for both `enabled` and `backend` | | R1 | `pgAutoConfigSkippedWhenBackendRedis` doesn't exercise the crash path (shared runner has a `DataSource`) | ✅ Fixed — dedicated `pgAutoConfigSkippedWhenBackendRedisNoDataSourceDoesNotCrash` test at line 82 uses a completely fresh runner with no `DataSource`, `KafkaTemplate`, or `OutboxRecordRepository` | | R1 | Class Javadoc falsely claimed no `DataSource` was present | ✅ Fixed — Javadoc at line 21–26 now explicitly calls out that the shared runner *does* supply a mock `DataSource` | | R2 | SpEL injection surface via `${…}` interpolation in `@ConditionalOnExpression` | ✅ Fixed — `@ConditionalOnExpression` replaced entirely with `@Conditional(OnPgOutboxBackendCondition.class)`; condition reads `Environment` directly in Java with no SpEL involved | **Implementation quality**: `OnPgOutboxBackendCondition` is correct and minimal. `ConditionContext.getEnvironment()` is guaranteed non-null by Spring's contract; the default-value overloads ensure `null`-safety; the class is intentionally package-private (Spring processes `@Conditional` via reflection so visibility is irrelevant). The `@AutoConfigureAfter` entries are pre-existing and Spring handles gracefully-absent ordering targets. **Test coverage**: Seven complementary tests cover the open path (default, explicit PG, mixed case), the closed path (REDIS with and without `DataSource`, lowercase redis, disabled), and the exact crash scenario that motivated the PR. No actionable new findings. ### CI status (head `ac825147eb59`) **Overall: ✗ failure** 2 checks: 2 pending | Check | State | Link | |---|---|---| | im2be-platform-libs CI / mvn install (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/73/jobs/0) | | im2be-platform-libs CI / mvn verify (main only) (pull_request) | ⏳ pending | [details](/affinity-intelligence-rework/im2be-platform-libs/actions/runs/73/jobs/1) | ### Findings **No new findings this round.** _Quorum converged on empty findings (A + B both returned 0)._ ### Verdict **NO_NEW_FINDINGS** --- <sub>hib-pr-reviewer • round 3 • 0 findings • 2026-05-28T13:40:04.359Z → 2026-05-28T13:42:16.004Z • posted-as: pr-reviewer-bot • [bookkeeping fallback]</sub>
hibryda deleted branch feat/outbox-pg-backend-gate 2026-05-28 15:43:20 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
affinity-intelligence-rework/im2be-platform-libs!9
No description provided.