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(); + } }