feat: Emit OTel log signal with anonymous user-id for bypass routes (#4704)#4716
feat: Emit OTel log signal with anonymous user-id for bypass routes (#4704)#4716balhar-jakub wants to merge 7 commits into
Conversation
…ter chain (#4704) - Chain .anonymousUserId() to set user.id='anonymous' for bypass routes - Chain .authenticationSuccess() to set auth.status='OK' for bypass routes - Both appended after .instanceId() in the apply() builder chain
…es (#4704) OtelRequestContextTest: - ANONYMOUS_USER_ID constant equals 'anonymous' - anonymousUserId() stores lowercase 'anonymous' (not uppercased) - verify anonymousUserId() does NOT store uppercased value OtelServiceFilterFactoryTest: - verify user.id='anonymous' and auth.status='OK' for bypass routes
…st (#4704) Change assertNull to assertEquals('anonymous') for user.id and assertEquals('OK') for auth.status in the WhenServiceBypass > thenLog test — the OtelServiceFilterFactory now sets these attributes for bypass routes via anonymousUserId() and authenticationSuccess().
Architectural Review — PR #4716 (#4704)Verdict: ✅ APPROVEDDesign ComplianceAll changes follow the Step 2 solution design precisely:
Structural Integrity
Edge Cases Covered
The 500 case correctly distinguishes routing failure from authentication failure — bypass auth always succeeds, so Test Coverage
Build
Non-blocking Observations
|
QA Review — PASSEDAcceptance Criteria Verification
Unit Tests
Pavel Lens — 8 Rules
Verdict: PASSED2 production files changed (+8/-1), 4 test files (+225/-2). Clean, minimal change. Tests verified passing. No blocking issues found. |
Security Review — APPROVEDPR: #4716 | Issue: #4704 | Reviewer: security-analyst Pavel Lens — 8 Rules Applied
Security Assessment (6 Categories)1. Auth/authz — PASS 2. Input Validation — PASS 3. Data Exposure — PASS (Net Improvement) 4. Dependencies — PASS 5. Configuration — PASS 6. Secrets Management — PASS OWASP Top 10 ScanNone triggered. A09 (Logging and Monitoring Failures) is actually improved by this PR — bypass route signals are now distinguishable from unrecorded signals. Verdict: ✅ APPROVEDNo CRITICAL, HIGH, or MEDIUM findings. This is a net security improvement: replacing null with explicit sentinel values ( Non-blocking: The |
Architectural Review (Rework Verification) — PR #4716 (#4704)Rework VerificationThe previous build failure (
Build Validation
Original Architectural Verdict Stands: ✅ APPROVEDAll findings from the original review (design compliance, structural integrity, edge cases, test coverage) remain valid. The rework is clean and minimal. |
QA Review — PASSED (Formal Protocol Completion)Acceptance Criteria Verification
Tests Verified
Pavel Lens — 8 Rules
Verdict: ✅ PASSEDAll 3 gates approved: Architect ✓, QA ✓, Security ✓. Ready for human review and merge. |
QA Re-verification — PASSEDTrigger: Re-verification after documentation-only Step 4c (docs-site changes, no code changes to this PR). Verification
Existing Reviews Stand
Pavel Lens — 8 Rules (re-verified)All rules remain N/A or OK — no code changes means no new config, no new duplication, no new null paths, no new tests to parametrize, no new security boundaries, no new logs, no new TODOs. Verdict: ✅ PASSED (re-confirmation)All 3 gates approved. Code unchanged. Ready for human review and merge. |
#4704) OtelServiceFilterFactory was unconditionally setting user.id='anonymous' and auth.status='OK' for ALL routes, overwriting values set by the real auth scheme filter (AbstractAuthSchemeFactory, etc.). Add authenticationScheme config field to OtelServiceFilterFactory.Config and only chain .anonymousUserId() + .authenticationSuccess() for BYPASS routes. Pass the scheme from RouteLocator.getPostRoutingFilters() via the new Authentication auth parameter.
Architect Review — Re-validation after rework (commit f844263)Verdict: APPROVED ✅The conditional bypass fix in What was reviewed1. Conditional logic in
2. RouteLocator signature change (L114)
3.
4. Test coverage
5. Build validation
Design compliance
No blocking issues foundHanding off to QA for acceptance criteria verification and Pavel lens review. |
QA Review — PASSED (Re-verification)Trigger: New dispatch of Step 7 QA for PR #4716 (#4704). Verification
Acceptance Criteria
Pavel Lens — 8 Rules
Verdict: ✅ PASSEDAll prior gates (Architect, QA, Security) stand. Code unchanged. Ready for human review. |
| void setUp() { | ||
| assertTrue(logExporter instanceof InMemoryLogRecordExporter, | ||
| "Expected InMemoryLogRecordExporter, got " + logExporter.getClass().getName()); | ||
| ((InMemoryLogRecordExporter) logExporter).reset(); |
There was a problem hiding this comment.
Is it possible to type already on the field to InMemoryLogRecordExporter?
There was a problem hiding this comment.
And thus remove this whole setUp method?
| "otel.logs.exporter=none" | ||
| } | ||
| ) | ||
| @DirtiesContext |
There was a problem hiding this comment.
Is it necessary to DirtyContext? Is there some alternative way to test, as DirtiesContext is actually quite expensive operation?
There was a problem hiding this comment.
Is this the same approach that is taken for testing the other OTel pieces?
| import static io.restassured.RestAssured.given; | ||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| class OpenTelemetryAnonymousBypassTest { |
There was a problem hiding this comment.
At least the minimum documentation on this method is missing.
| .findFirst() | ||
| .orElseThrow(() -> new AssertionError( | ||
| "Expected log record with URL " + expectedUrl + " not found in logs: " | ||
| + logs.stream().map(LogRecordData::getBodyValue).map(String::valueOf).collect(Collectors.joining(", ")))); |
There was a problem hiding this comment.
Why do you getBodyValue, then map it, collect it and then again later on do something very similar again or even build JSON out of it?
| var logBody = logRecord.getBodyValue().asString(); | ||
|
|
||
| // Full attribute set verification | ||
| assertEquals("GET", getAttribute(logBody, "http.request.method"), |
There was a problem hiding this comment.
Each of the lines below is always reconverting the logBody within getAttribute, is this actually a good implementation?
| otelRequestBasicFilter.setName("OtelServiceFilterFactory"); | ||
| otelRequestBasicFilter.addArg("serviceId", serviceInstance.getServiceId()); | ||
| otelRequestBasicFilter.addArg("instanceId", serviceInstance.getInstanceId()); | ||
| if (auth != null && auth.getScheme() != null) { |
There was a problem hiding this comment.
Is there a security issue with exposing this information?
…anup - Make AUTH_STATUS_OK/AUTH_STATUS_ERROR public constants (non-blocking feedback) - Remove redundant thenNotUppercase test - Add test for non-BYPASS path in OtelServiceFilterFactory (coverage gap) - Add test for auth!=null branch in RouteLocator (coverage gap)
…test - Rename BASIC_AUTH_TYPE -> AUTH_TYPE_BASIC for naming consistency with AUTH_STATUS_OK, AUTH_STATUS_ERROR, and ANONYMOUS_USER_ID constants - Add Javadoc for Config.authenticationScheme field documenting its purpose - Add test for explicitly non-BYPASS authenticationScheme (e.g. ZOWE_JWT) to complement the existing null-case test
|



Closes #4704
Summary
When APIML routes a request through a bypass-authentication service, the OTel log signal now includes
user.id=anonymousandauth.status=OKinstead of leaving these attributes unset.Changes
ANONYMOUS_USER_IDconstant andanonymousUserId()method (stores lowercase, unlikeuserId()which uppercases)anonymousUserId()andauthenticationSuccess()in the bypass filter chainassertNullto expectanonymous/OKBuild
:gateway-service:build— PASSED (all unit tests pass):apiml:testhas pre-existing SAF dependency failures unrelated to this change