From ad5f21aa5ef2d1376e3ce865b44e35936a80a3c2 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:04:36 -0500 Subject: [PATCH 1/4] User auth support --- pom.xml | 6 + .../handlers/cache/PostCacheHandler.java | 43 +++++-- .../prebid/cache/metrics/MeasurementTag.java | 1 + .../prebid/cache/metrics/MetricsRecorder.java | 11 ++ .../org/prebid/cache/routers/ApiConfig.java | 4 + .../cache/handlers/PostCacheHandlerTests.java | 115 ++++++++++++++++++ 6 files changed, 173 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 073f468..24aaf7f 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ 4.2.0 2.1.0 2.20.0 + 4.6.0 5.9.1 @@ -150,6 +151,11 @@ org.apache.commons commons-lang3 + + org.apache.commons + commons-collections4 + ${commons-collections4.version} + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java index 21eecfe..33120c3 100644 --- a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java +++ b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java @@ -5,6 +5,7 @@ import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; import io.netty.channel.ChannelOption; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.prebid.cache.builders.PrebidServerResponseBuilder; @@ -50,6 +51,9 @@ import java.util.function.Consumer; import java.util.function.Function; +import static org.apache.commons.collections4.MapUtils.emptyIfNull; +import static org.apache.commons.collections4.MapUtils.invertMap; + @Component @Slf4j public class PostCacheHandler extends CacheHandler { @@ -57,6 +61,7 @@ public class PostCacheHandler extends CacheHandler { private static final String UUID_KEY = "uuid"; private static final String SECONDARY_CACHE_KEY = "secondaryCache"; private static final String API_KEY_HEADER = "x-pbc-api-key"; + private static final String USER_KEY_PARAM = "x-pbc-user-key"; private final ReactiveRepository repository; private final CacheConfig config; @@ -66,6 +71,7 @@ public class PostCacheHandler extends CacheHandler { private final ObjectMapper objectMapper = new ObjectMapper(); private final CircuitBreaker circuitBreaker; private final ApiConfig apiConfig; + private final Map userKeys; public PostCacheHandler(final ReactiveRepository repository, final CacheConfig config, @@ -95,15 +101,14 @@ public PostCacheHandler(final ReactiveRepository reposit this.metricTagPrefix = "write"; this.circuitBreaker = webClientCircuitBreaker; this.apiConfig = apiConfig; + this.userKeys = invertMap(emptyIfNull(apiConfig.getUserKeys())); } public Mono save(final ServerRequest request) { - final boolean isValidApiKey = isValidApiKey(request); - if (isValidApiKey) { - metricsRecorder.markMeterForTag(metricTagPrefix, MeasurementTag.REQUEST_TRUSTED); - } + final boolean isValidApiKey = validateApiKey(request); + final boolean isValidUserKey = validateUserKey(request); - if (apiConfig.isCacheWriteSecured() && !isValidApiKey) { + if (apiConfig.isCacheWriteSecured() && (!isValidApiKey || !isValidUserKey)) { metricsRecorder.markMeterForTag(metricTagPrefix, MeasurementTag.ERROR_UNAUTHORIZED); return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); } @@ -168,8 +173,32 @@ private void validateUuidPermissions(PayloadWrapper payload, sink.next(payload); } - private boolean isValidApiKey(final ServerRequest request) { - return StringUtils.equals(request.headers().firstHeader(API_KEY_HEADER), apiConfig.getApiKey()); + private boolean validateApiKey(final ServerRequest request) { + final boolean isValidApiKey = StringUtils.equals(request.headers().firstHeader(API_KEY_HEADER), + apiConfig.getApiKey()); + + if (isValidApiKey) { + metricsRecorder.markMeterForTag(metricTagPrefix, MeasurementTag.REQUEST_TRUSTED); + } + + return isValidApiKey; + } + + private boolean validateUserKey(ServerRequest request) { + final String userKey = ObjectUtils.firstNonNull(request.headers().firstHeader(USER_KEY_PARAM), + request.queryParams().getFirst(USER_KEY_PARAM)); + + if (StringUtils.isEmpty(userKey)) { + return false; + } + + final String user = userKeys.get(userKey); + if (user != null) { + metricsRecorder.markMeterForUserTag(metricTagPrefix, user, MeasurementTag.REQUEST_USER); + return true; + } + + return false; } private Function payloadWrapperTransformer() { diff --git a/src/main/java/org/prebid/cache/metrics/MeasurementTag.java b/src/main/java/org/prebid/cache/metrics/MeasurementTag.java index b8d9913..ae99dee 100644 --- a/src/main/java/org/prebid/cache/metrics/MeasurementTag.java +++ b/src/main/java/org/prebid/cache/metrics/MeasurementTag.java @@ -7,6 +7,7 @@ public enum MeasurementTag { REQUEST_DURATION("pbc.${prefix}.request.duration"), REQUEST("pbc.${prefix}.request"), REQUEST_TRUSTED("pbc.${prefix}.trustedRequest"), + REQUEST_USER("pbc.${prefix}.${user}.request"), REQUEST_INVALID("pbc.request.invalid"), ERROR_UNKNOWN("pbc.${prefix}.err.unknown"), ERROR_TIMED_OUT("pbc.${prefix}.err.timedOut"), diff --git a/src/main/java/org/prebid/cache/metrics/MetricsRecorder.java b/src/main/java/org/prebid/cache/metrics/MetricsRecorder.java index 200e243..bcac280 100644 --- a/src/main/java/org/prebid/cache/metrics/MetricsRecorder.java +++ b/src/main/java/org/prebid/cache/metrics/MetricsRecorder.java @@ -15,6 +15,7 @@ public class MetricsRecorder { protected static final String PREFIX_PLACEHOLDER = "\\$\\{prefix\\}"; protected static final String TTL_BUCKET_PLACEHOLDER = "\\$\\{ttlBucket\\}"; + protected static final String USER_PLACEHOLDER = "\\$\\{user\\}"; public MetricsRecorder(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; @@ -62,10 +63,20 @@ private Counter meterForTag(final String prefix, final MeasurementTag measuremen return meterRegistry.counter(measurementTag.getTag().replaceAll(PREFIX_PLACEHOLDER, prefix)); } + private Counter meterForUserTag(String prefix, String user, MeasurementTag measurementTag) { + return meterRegistry.counter(measurementTag.getTag() + .replaceAll(PREFIX_PLACEHOLDER, prefix) + .replaceAll(USER_PLACEHOLDER, user)); + } + public void markMeterForTag(final String prefix, final MeasurementTag measurementTag) { meterForTag(prefix, measurementTag).increment(); } + public void markMeterForUserTag(String prefix, String user, MeasurementTag measurementTag) { + meterForUserTag(prefix, user, measurementTag).increment(); + } + public MetricsRecorderTimer createRequestTimerForServiceType(final ServiceType serviceType) { if (serviceType.equals(ServiceType.FETCH)) { return new MetricsRecorderTimer( diff --git a/src/main/java/org/prebid/cache/routers/ApiConfig.java b/src/main/java/org/prebid/cache/routers/ApiConfig.java index 6ef1a8b..3aae6cc 100644 --- a/src/main/java/org/prebid/cache/routers/ApiConfig.java +++ b/src/main/java/org/prebid/cache/routers/ApiConfig.java @@ -8,6 +8,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.validation.annotation.Validated; +import java.util.Map; + @Data @NoArgsConstructor @AllArgsConstructor @@ -28,6 +30,8 @@ public class ApiConfig { @NotEmpty private String apiKey; + private Map userKeys; + // workaround for supporting transition period of deprecated path property public void setPath(String path) { cachePath = path; diff --git a/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java b/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java index f2dfdb5..0cba73a 100644 --- a/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java +++ b/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.Date; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; @@ -334,4 +335,118 @@ void testUuidAuthorizationWithValidApiKey() { .expectComplete() .verify(); } + + @Test + void testWriteAuthorizationWithValidUserKeyHeader() { + given(apiConfig.isCacheWriteSecured()).willReturn(true); + given(apiConfig.getUserKeys()).willReturn(Map.of("user-name", "user-key")); + given(repository.save(PAYLOAD_WRAPPER)).willReturn(Mono.just(PAYLOAD_WRAPPER)); + + final var handler = new PostCacheHandler( + repository, + cacheConfig, + metricsRecorder, + builder, + webClientCircuitBreaker, + samplingRate, + apiConfig); + + final var request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); + final var requestMono = MockServerRequest.builder() + .method(HttpMethod.POST) + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("x-pbc-user-key", "user-key") + .body(request); + + StepVerifier.create(handler.save(requestMono)) + .consumeNextWith(serverResponse -> assertEquals(200, serverResponse.statusCode().value())) + .expectComplete() + .verify(); + } + + @Test + void testWriteAuthorizationWithValidUserKeyQueryParameter() { + given(apiConfig.isCacheWriteSecured()).willReturn(true); + given(apiConfig.getUserKeys()).willReturn(Map.of("user-name", "user-key")); + given(repository.save(PAYLOAD_WRAPPER)).willReturn(Mono.just(PAYLOAD_WRAPPER)); + + final var handler = new PostCacheHandler( + repository, + cacheConfig, + metricsRecorder, + builder, + webClientCircuitBreaker, + samplingRate, + apiConfig); + + final var request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); + final var requestMono = MockServerRequest.builder() + .method(HttpMethod.POST) + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .queryParam("x-pbc-user-key", "user-key") + .body(request); + + StepVerifier.create(handler.save(requestMono)) + .consumeNextWith(serverResponse -> assertEquals(200, serverResponse.statusCode().value())) + .expectComplete() + .verify(); + } + + @Test + void testWriteAuthorizationWithValidUserKeyShouldPrioritizeHeader() { + given(apiConfig.isCacheWriteSecured()).willReturn(true); + given(apiConfig.getUserKeys()).willReturn(Map.of("user-name", "user-key")); + given(repository.save(PAYLOAD_WRAPPER)).willReturn(Mono.just(PAYLOAD_WRAPPER)); + + final var handler = new PostCacheHandler( + repository, + cacheConfig, + metricsRecorder, + builder, + webClientCircuitBreaker, + samplingRate, + apiConfig); + + final var request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); + final var requestMono = MockServerRequest.builder() + .method(HttpMethod.POST) + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("x-pbc-user-key", "user-key") + .queryParam("x-pbc-user-key", "invalid") + .body(request); + + StepVerifier.create(handler.save(requestMono)) + .consumeNextWith(serverResponse -> assertEquals(200, serverResponse.statusCode().value())) + .expectComplete() + .verify(); + } + + @Test + void testWriteAuthorizationWithInvalidUserKey() { + given(apiConfig.isCacheWriteSecured()).willReturn(true); + given(apiConfig.getUserKeys()).willReturn(Map.of("user-name", "user-key")); + given(repository.save(PAYLOAD_WRAPPER)).willReturn(Mono.just(PAYLOAD_WRAPPER)); + + final var handler = new PostCacheHandler( + repository, + cacheConfig, + metricsRecorder, + builder, + webClientCircuitBreaker, + samplingRate, + apiConfig); + + final var request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); + final var requestMono = MockServerRequest.builder() + .method(HttpMethod.POST) + .header(CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .header("x-pbc-user-key", "invalid") + .queryParam("x-pbc-user-key", "user-key") + .body(request); + + StepVerifier.create(handler.save(requestMono)) + .consumeNextWith(serverResponse -> assertEquals(401, serverResponse.statusCode().value())) + .expectComplete() + .verify(); + } } From fb1216e87451b900addd2b13624b32b0eacbd5c3 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:03:14 -0500 Subject: [PATCH 2/4] Fix API key check condition --- .../prebid/cache/handlers/cache/PostCacheHandler.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java index 33120c3..646cb47 100644 --- a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java +++ b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java @@ -108,7 +108,7 @@ public Mono save(final ServerRequest request) { final boolean isValidApiKey = validateApiKey(request); final boolean isValidUserKey = validateUserKey(request); - if (apiConfig.isCacheWriteSecured() && (!isValidApiKey || !isValidUserKey)) { + if (apiConfig.isCacheWriteSecured() && !(isValidApiKey || isValidUserKey)) { metricsRecorder.markMeterForTag(metricTagPrefix, MeasurementTag.ERROR_UNAUTHORIZED); return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); } @@ -174,8 +174,8 @@ private void validateUuidPermissions(PayloadWrapper payload, } private boolean validateApiKey(final ServerRequest request) { - final boolean isValidApiKey = StringUtils.equals(request.headers().firstHeader(API_KEY_HEADER), - apiConfig.getApiKey()); + final boolean isValidApiKey = apiConfig.getApiKey() != null + && StringUtils.equals(request.headers().firstHeader(API_KEY_HEADER), apiConfig.getApiKey()); if (isValidApiKey) { metricsRecorder.markMeterForTag(metricTagPrefix, MeasurementTag.REQUEST_TRUSTED); @@ -286,7 +286,8 @@ private Consumer enrichWithSecurityHeader() { final String apiKey = apiConfig.getApiKey(); return apiKey != null ? httpHeaders -> httpHeaders.set(API_KEY_HEADER, apiKey) - : httpHeaders -> { }; + : httpHeaders -> { + }; } private PayloadTransfer wrapperToTransfer(final PayloadWrapper wrapper) { From 880d0a69ba95e57477327739f1701a4ce225ffcf Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:12:04 -0500 Subject: [PATCH 3/4] Fix Checkstyle --- .../java/org/prebid/cache/handlers/cache/PostCacheHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java index 646cb47..77d1072 100644 --- a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java +++ b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java @@ -286,8 +286,7 @@ private Consumer enrichWithSecurityHeader() { final String apiKey = apiConfig.getApiKey(); return apiKey != null ? httpHeaders -> httpHeaders.set(API_KEY_HEADER, apiKey) - : httpHeaders -> { - }; + : httpHeaders -> { }; } private PayloadTransfer wrapperToTransfer(final PayloadWrapper wrapper) { From 438a80bbcd9e6ef500e65f7624bd992287f62b35 Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:01:01 -0500 Subject: [PATCH 4/4] Remark fixes --- .../cache/handlers/cache/PostCacheHandler.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java index 77d1072..5f15430 100644 --- a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java +++ b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java @@ -5,6 +5,7 @@ import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; import io.netty.channel.ChannelOption; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -51,9 +52,6 @@ import java.util.function.Consumer; import java.util.function.Function; -import static org.apache.commons.collections4.MapUtils.emptyIfNull; -import static org.apache.commons.collections4.MapUtils.invertMap; - @Component @Slf4j public class PostCacheHandler extends CacheHandler { @@ -101,7 +99,7 @@ public PostCacheHandler(final ReactiveRepository reposit this.metricTagPrefix = "write"; this.circuitBreaker = webClientCircuitBreaker; this.apiConfig = apiConfig; - this.userKeys = invertMap(emptyIfNull(apiConfig.getUserKeys())); + this.userKeys = MapUtils.invertMap(MapUtils.emptyIfNull(apiConfig.getUserKeys())); } public Mono save(final ServerRequest request) { @@ -185,14 +183,11 @@ private boolean validateApiKey(final ServerRequest request) { } private boolean validateUserKey(ServerRequest request) { - final String userKey = ObjectUtils.firstNonNull(request.headers().firstHeader(USER_KEY_PARAM), + final String userKey = ObjectUtils.firstNonNull( + request.headers().firstHeader(USER_KEY_PARAM), request.queryParams().getFirst(USER_KEY_PARAM)); - if (StringUtils.isEmpty(userKey)) { - return false; - } - - final String user = userKeys.get(userKey); + final String user = StringUtils.isNotEmpty(userKey) ? userKeys.get(userKey) : null; if (user != null) { metricsRecorder.markMeterForUserTag(metricTagPrefix, user, MeasurementTag.REQUEST_USER); return true;