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..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,8 @@
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;
import org.prebid.cache.builders.PrebidServerResponseBuilder;
@@ -57,6 +59,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 +69,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 +99,14 @@ public PostCacheHandler(final ReactiveRepository reposit
this.metricTagPrefix = "write";
this.circuitBreaker = webClientCircuitBreaker;
this.apiConfig = apiConfig;
+ this.userKeys = MapUtils.invertMap(MapUtils.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 +171,29 @@ 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 = apiConfig.getApiKey() != null
+ && 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));
+
+ final String user = StringUtils.isNotEmpty(userKey) ? userKeys.get(userKey) : null;
+ 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();
+ }
}