From 5263281f4e551f09213cdc7890f04c594358e216 Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Mon, 7 Oct 2024 14:33:36 +0300 Subject: [PATCH 1/3] Added simple api-key authorization for post cache handler. --- .../handlers/cache/PostCacheHandler.java | 16 +++++- .../org/prebid/cache/routers/ApiConfig.java | 2 + .../cache/handlers/PostCacheHandlerTests.java | 52 ++++++++++++++----- 3 files changed, 57 insertions(+), 13 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 ff4d997..42867da 100644 --- a/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java +++ b/src/main/java/org/prebid/cache/handlers/cache/PostCacheHandler.java @@ -22,6 +22,7 @@ import org.prebid.cache.model.ResponseObject; import org.prebid.cache.repository.CacheConfig; import org.prebid.cache.repository.ReactiveRepository; +import org.prebid.cache.routers.ApiConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; @@ -49,6 +50,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 final ReactiveRepository repository; private final CacheConfig config; @@ -57,6 +59,7 @@ public class PostCacheHandler extends CacheHandler { private final Map webClients = new HashMap<>(); private final ObjectMapper objectMapper = new ObjectMapper(); private final CircuitBreaker circuitBreaker; + private final ApiConfig apiConfig; @Autowired public PostCacheHandler(final ReactiveRepository repository, @@ -64,7 +67,8 @@ public PostCacheHandler(final ReactiveRepository reposit final MetricsRecorder metricsRecorder, final PrebidServerResponseBuilder builder, final CircuitBreaker webClientCircuitBreaker, - @Value("${sampling.rate:0.01}") final Double samplingRate) { + @Value("${sampling.rate:0.01}") final Double samplingRate, + final ApiConfig apiConfig) { super(samplingRate); this.metricsRecorder = metricsRecorder; @@ -77,9 +81,14 @@ public PostCacheHandler(final ReactiveRepository reposit this.builder = builder; this.metricTagPrefix = "write"; this.circuitBreaker = webClientCircuitBreaker; + this.apiConfig = apiConfig; } public Mono save(final ServerRequest request) { + if (!isWriteAllowed(request)) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); + } + metricsRecorder.markMeterForTag(this.metricTagPrefix, MetricsRecorder.MeasurementTag.REQUEST); final var timerContext = metricsRecorder.createRequestTimerForServiceType(type); @@ -119,6 +128,11 @@ public Mono save(final ServerRequest request) { return finalizeResult(responseMono, request, timerContext); } + private boolean isWriteAllowed(final ServerRequest request) { + return !apiConfig.isCacheWriteSecured() + || StringUtils.equals(request.headers().firstHeader(API_KEY_HEADER), apiConfig.getApiKey()); + } + private Function payloadWrapperTransformer() { return transfer -> PayloadWrapper.builder() .id(RandomUUID.extractUUID(transfer)) diff --git a/src/main/java/org/prebid/cache/routers/ApiConfig.java b/src/main/java/org/prebid/cache/routers/ApiConfig.java index cf5216f..18f77e8 100644 --- a/src/main/java/org/prebid/cache/routers/ApiConfig.java +++ b/src/main/java/org/prebid/cache/routers/ApiConfig.java @@ -19,6 +19,8 @@ public class ApiConfig { @NotEmpty private String cachePath; + private boolean cacheWriteSecured; + @NotEmpty private String storagePath; diff --git a/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java b/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java index d251c11..b5386f6 100644 --- a/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java +++ b/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java @@ -81,6 +81,9 @@ class PostCacheHandlerTests extends CacheHandlerTests { @MockBean ReactiveRepository repository; + @MockBean + ApiConfig apiConfig; + @Value("${sampling.rate:2.0}") Double samplingRate; @@ -92,7 +95,8 @@ void testVerifyError() { metricsRecorder, builder, webClientCircuitBreaker, - samplingRate); + samplingRate, + apiConfig); verifyJacksonError(handler); verifyRepositoryError(handler); } @@ -116,7 +120,7 @@ void testVerifySave() { given(repository.save(PAYLOAD_WRAPPER)).willReturn(Mono.just(PAYLOAD_WRAPPER)); final PostCacheHandler handler = new PostCacheHandler(repository, cacheConfig, metricsRecorder, builder, - webClientCircuitBreaker, samplingRate); + webClientCircuitBreaker, samplingRate, apiConfig); final Mono request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); final MockServerRequest requestMono = MockServerRequest.builder() @@ -144,7 +148,7 @@ void testSecondaryCacheSuccess() { .willReturn(aResponse().withBody("{\"responses\":[{\"uuid\":\"2be04ba5-8f9b-4a1e-8100-d573c40312f8\"}]}"))); final PostCacheHandler handler = new PostCacheHandler(repository, cacheConfig, metricsRecorder, builder, - webClientCircuitBreaker, samplingRate); + webClientCircuitBreaker, samplingRate, apiConfig); final Mono request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); final MockServerRequest requestMono = MockServerRequest.builder() @@ -173,11 +177,11 @@ void testSecondaryCacheSuccess() { void testExternalUUIDInvalid() { //given final var cacheConfigLocal = new CacheConfig(cacheConfig.getPrefix(), cacheConfig.getExpirySec(), - cacheConfig.getTimeoutMs(), - cacheConfig.getMinExpiry(), cacheConfig.getMaxExpiry(), - false, Collections.emptyList(), cacheConfig.getSecondaryCachePath(), 100, 100, "example.com", "http"); + cacheConfig.getTimeoutMs(), + cacheConfig.getMinExpiry(), cacheConfig.getMaxExpiry(), + false, Collections.emptyList(), cacheConfig.getSecondaryCachePath(), 100, 100, "example.com", "http"); final var handler = new PostCacheHandler(repository, cacheConfigLocal, metricsRecorder, builder, - webClientCircuitBreaker, samplingRate); + webClientCircuitBreaker, samplingRate, apiConfig); final Mono request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); final MockServerRequest requestMono = MockServerRequest.builder() @@ -208,7 +212,7 @@ void testUUIDDuplication() { 5, cacheConfig.getMaxExpiry(), cacheConfig.isAllowExternalUUID(), Collections.emptyList(), cacheConfig.getSecondaryCachePath(), 100, 100, "example.com", "http"); final PostCacheHandler handler = new PostCacheHandler(repository, cacheConfigLocal, metricsRecorder, builder, - webClientCircuitBreaker, samplingRate); + webClientCircuitBreaker, samplingRate, apiConfig); final Mono request = Mono.just(RequestObject.of(Collections.singletonList(PAYLOAD_TRANSFER))); final MockServerRequest requestMono = MockServerRequest.builder() @@ -229,11 +233,35 @@ void testUUIDDuplication() { final Mono responseMonoSecond = handler.save(requestMono); final Consumer consumerSecond = serverResponse -> - assertEquals(400, serverResponse.statusCode().value()); + assertEquals(400, serverResponse.statusCode().value()); StepVerifier.create(responseMonoSecond) - .consumeNextWith(consumerSecond) - .expectComplete() - .verify(); + .consumeNextWith(consumerSecond) + .expectComplete() + .verify(); + } + + @Test + void testAuthorization() { + given(apiConfig.isCacheWriteSecured()).willReturn(true); + given(apiConfig.getApiKey()).willReturn("api-key"); + + var handler = new PostCacheHandler( + repository, + cacheConfig, + metricsRecorder, + builder, + webClientCircuitBreaker, + samplingRate, + apiConfig); + + var request = MockServerRequest.builder() + .method(HttpMethod.POST) + .build(); + + StepVerifier.create(handler.save(request)) + .consumeNextWith(serverResponse -> assertEquals(401, serverResponse.statusCode().value())) + .expectComplete() + .verify(); } } From 6e696b43c3d2057c184b5ce37aab7d1bea3faf8a Mon Sep 17 00:00:00 2001 From: Alex Maltsev Date: Mon, 7 Oct 2024 14:55:32 +0300 Subject: [PATCH 2/3] Fixed styling. --- .../java/org/prebid/cache/handlers/PostCacheHandlerTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java b/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java index b5386f6..2004470 100644 --- a/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java +++ b/src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java @@ -246,7 +246,7 @@ void testAuthorization() { given(apiConfig.isCacheWriteSecured()).willReturn(true); given(apiConfig.getApiKey()).willReturn("api-key"); - var handler = new PostCacheHandler( + final var handler = new PostCacheHandler( repository, cacheConfig, metricsRecorder, @@ -255,7 +255,7 @@ void testAuthorization() { samplingRate, apiConfig); - var request = MockServerRequest.builder() + final var request = MockServerRequest.builder() .method(HttpMethod.POST) .build(); From 1191c5b6b69f0d8381840122bfbb84c6b3fcdcef Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:16:26 +0200 Subject: [PATCH 3/3] Add functional tests for prevent external (#142) * Add draft functional tests for prevent external POST --- .../cache/functional/AerospikeCacheSpec.kt | 26 +-- .../cache/functional/ApacheIgniteCacheSpec.kt | 22 +-- .../functional/AuthenticationCacheSpec.kt | 184 ++++++++++++++++++ .../org/prebid/cache/functional/BaseSpec.kt | 2 +- .../cache/functional/GeneralCacheSpec.kt | 12 +- .../cache/functional/ProxyCacheHostSpec.kt | 4 +- .../prebid/cache/functional/RedisCacheSpec.kt | 8 +- .../cache/functional/SecondaryCacheSpec.kt | 2 +- .../functional/service/PrebidCacheApi.kt | 37 +++- .../PrebidCacheContainerConfig.kt | 73 ++++--- 10 files changed, 302 insertions(+), 68 deletions(-) create mode 100644 src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt diff --git a/src/test/kotlin/org/prebid/cache/functional/AerospikeCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/AerospikeCacheSpec.kt index f1d3d97..4eb6831 100644 --- a/src/test/kotlin/org/prebid/cache/functional/AerospikeCacheSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/AerospikeCacheSpec.kt @@ -25,7 +25,7 @@ class AerospikeCacheSpec : ShouldSpec({ should("throw an exception when cache record is absent in Aerospike repository") { // given: Prebid cache config - val config = prebidCacheConfig.getBaseAerospikeConfig("true") + val config = prebidCacheConfig.getBaseAerospikeConfig(true) val cachePrefix = config["cache.prefix"] // when: GET cache endpoint with random UUID is called @@ -42,7 +42,7 @@ class AerospikeCacheSpec : ShouldSpec({ should("rethrow an exception from Aerospike cache server when such happens") { // given: Prebid Cache with not matched to Aerospike server namespace val unmatchedNamespace = getRandomString() - val config = prebidCacheConfig.getBaseAerospikeConfig("true", unmatchedNamespace) + val config = prebidCacheConfig.getBaseAerospikeConfig(true, unmatchedNamespace) val prebidCacheApi = BaseSpec.getPrebidCacheApi(config) // and: Default request object @@ -64,8 +64,8 @@ class AerospikeCacheSpec : ShouldSpec({ should("throw an exception when aerospike.prevent_UUID_duplication=true and request with already existing UUID is send") { // given: Prebid Cache with aerospike.prevent_UUID_duplication=true property val prebidCacheApi = BaseSpec.getPrebidCacheApi( - prebidCacheConfig.getBaseAerospikeConfig("true") + - prebidCacheConfig.getAerospikePreventUuidDuplicationConfig("true") + prebidCacheConfig.getBaseAerospikeConfig(true) + + prebidCacheConfig.getAerospikePreventUuidDuplicationConfig(true) ) // and: First request object with set UUID @@ -73,10 +73,10 @@ class AerospikeCacheSpec : ShouldSpec({ val xmlPayloadTransfer = PayloadTransfer.getDefaultXmlPayloadTransfer().apply { key = uuid } val requestObject = RequestObject.of(xmlPayloadTransfer) - // and: First request object is saved to Aerospike cache + // and: The first request object is saved to Aerospike cache prebidCacheApi.postCache(requestObject) - // and: Second request object with already existing UUID is prepared + // and: A second request object with already existing UUID is prepared val jsonPayloadTransfer = PayloadTransfer.getDefaultJsonPayloadTransfer().apply { key = uuid } val secondRequestObject = RequestObject.of(jsonPayloadTransfer) @@ -93,8 +93,8 @@ class AerospikeCacheSpec : ShouldSpec({ should("return back two request UUIDs when allow_external_UUID=true and 2 payload transfers were successfully cached in Aerospike") { // given: Prebid Cache with allow_external_UUID=true property val prebidCacheApi = BaseSpec.getPrebidCacheApi( - prebidCacheConfig.getBaseAerospikeConfig("true") + - prebidCacheConfig.getAerospikePreventUuidDuplicationConfig("false") + prebidCacheConfig.getBaseAerospikeConfig(true) + + prebidCacheConfig.getAerospikePreventUuidDuplicationConfig(false) ) // and: Request object with 2 payload transfers and set UUIDs is prepared @@ -105,7 +105,7 @@ class AerospikeCacheSpec : ShouldSpec({ // when: POST cache endpoint is called val responseObject: ResponseObject = prebidCacheApi.postCache(requestObject) - // then: UUIDs from request object are returned + // then: UUIDs from a request object are returned responseObject.responses.isEmpty() shouldBe false responseObject.responses.size shouldBe 2 @@ -117,8 +117,8 @@ class AerospikeCacheSpec : ShouldSpec({ should("update existing cache record when aerospike.prevent_UUID_duplication=false and request with already existing UUID is send") { // given: Prebid Cache with aerospike.prevent_UUID_duplication=false val prebidCacheApi = BaseSpec.getPrebidCacheApi( - prebidCacheConfig.getBaseAerospikeConfig("true") + - prebidCacheConfig.getAerospikePreventUuidDuplicationConfig("false") + prebidCacheConfig.getBaseAerospikeConfig(true) + + prebidCacheConfig.getAerospikePreventUuidDuplicationConfig(false) ) // and: First request object @@ -126,10 +126,10 @@ class AerospikeCacheSpec : ShouldSpec({ val xmlPayloadTransfer = PayloadTransfer.getDefaultXmlPayloadTransfer().apply { key = uuid } val requestObject = RequestObject.of(xmlPayloadTransfer) - // and: First request object is saved to Aerospike cache + // and: The first request object is saved to Aerospike cache prebidCacheApi.postCache(requestObject) - // and: Second request object with already existing UUID is prepared + // and: A second request object with already existing UUID is prepared val jsonPayloadTransfer = PayloadTransfer.getDefaultJsonPayloadTransfer().apply { key = uuid } val secondRequestObject = RequestObject.of(jsonPayloadTransfer) val requestTransferValue = objectMapper.readValue(secondRequestObject.puts[0].value, TransferValue::class.java) diff --git a/src/test/kotlin/org/prebid/cache/functional/ApacheIgniteCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/ApacheIgniteCacheSpec.kt index fc59dd5..1098b8e 100644 --- a/src/test/kotlin/org/prebid/cache/functional/ApacheIgniteCacheSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/ApacheIgniteCacheSpec.kt @@ -26,7 +26,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("throw an exception when cache record is absent in Apache ignite repository") { // given: Prebid cache config - val config = prebidCacheConfig.getBaseApacheIgniteConfig("true") + val config = prebidCacheConfig.getBaseApacheIgniteConfig(true) val cachePrefix = config["cache.prefix"] // when: GET cache endpoint with random UUID is called @@ -42,7 +42,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("throw an exception when cache is absent in Apache ignite repository due to expiration") { // given: Prebid Cache with set min and max expiry as 0 - val config = prebidCacheConfig.getBaseApacheIgniteConfig("false") + + val config = prebidCacheConfig.getBaseApacheIgniteConfig(false) + prebidCacheConfig.getCacheExpiryConfig("0", "0") val prebidCacheApi = BaseSpec.getPrebidCacheApi(config) @@ -65,7 +65,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("rethrow an exception from Apache ignite cache server when such happens") { // given: Prebid Cache with not matched to Apache ignite server namespace - val config = prebidCacheConfig.getBaseApacheIgniteConfig("true", getRandomString()) + val config = prebidCacheConfig.getBaseApacheIgniteConfig(true, getRandomString()) val prebidCacheApi = BaseSpec.getPrebidCacheApi(config) // and: Default request object @@ -86,7 +86,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("return back PBC UUID when allow_external_UUID=false and request object was successfully cached in Apache ignite") { // given: Prebid Cache with disabled allow_external_UUID property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig("false")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig(false)) // and: Request object val requestObject = RequestObject.getDefaultJsonRequestObject() @@ -102,7 +102,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("return back two random UUIDs when allow_external_UUID=false and 2 payload transfers were successfully cached") { // given: Prebid Cache with disabled allow_external_UUID property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig("false")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig(false)) // given: Request object val xmlPayloadTransfer = PayloadTransfer.getDefaultXmlPayloadTransfer().apply { key = null } @@ -121,7 +121,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("return back a request UUID when allow_external_UUID=true and request object was successfully cached") { // given: Prebid Cache with enabled allow_external_UUID property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig(true)) // and: Request object with set payload transfer UUID key val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = getRandomUuid() } @@ -137,7 +137,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("return back two request UUIDs when allow_external_UUID=true and 2 payload transfers were successfully cached in Apache ignite") { // given: Prebid Cache with allow_external_UUID=true property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig(true)) // and: Request object with 2 payload transfers and set UUIDs is prepared val xmlPayloadTransfer = PayloadTransfer.getDefaultXmlPayloadTransfer().apply { key = getRandomUuid() } @@ -158,7 +158,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ should("shouldn't update existing cache record when request with already existing UUID is send") { // given: Prebid Cache with allow_external_UUID=true property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseApacheIgniteConfig(true)) // and: First request object val uuid = getRandomUuid() @@ -166,10 +166,10 @@ class ApacheIgniteCacheSpec : ShouldSpec({ val requestObject = RequestObject.of(xmlPayloadTransfer) val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java) - // and: First request object is saved to Aerospike cache + // and: The first request object is saved to Apache ignite cache prebidCacheApi.postCache(requestObject) - // and: Second request object with already existing UUID is prepared + // and: A second request object with already existing UUID is prepared val jsonPayloadTransfer = PayloadTransfer.getDefaultJsonPayloadTransfer().apply { key = uuid } val secondRequestObject = RequestObject.of(jsonPayloadTransfer) @@ -181,7 +181,7 @@ class ApacheIgniteCacheSpec : ShouldSpec({ responseObject.responses.size shouldBe 1 responseObject.responses[0].uuid shouldBe secondRequestObject.puts[0].key - // and: Cache record was updated in Aerospike with a second request object payload + // and: Cache record was updated in Apache ignite with a second request object payload val getCacheResponse = prebidCacheApi.getCache(responseObject.responses[0].uuid) val responseTransferValue = objectMapper.readValue(getCacheResponse.bodyAsText(), TransferValue::class.java) diff --git a/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt new file mode 100644 index 0000000..2149be3 --- /dev/null +++ b/src/test/kotlin/org/prebid/cache/functional/AuthenticationCacheSpec.kt @@ -0,0 +1,184 @@ +package org.prebid.cache.functional + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.beEmpty +import io.ktor.client.statement.bodyAsText +import io.ktor.http.contentType +import org.prebid.cache.functional.BaseSpec.Companion.prebidCacheConfig +import org.prebid.cache.functional.mapper.objectMapper +import org.prebid.cache.functional.model.request.RequestObject +import org.prebid.cache.functional.model.request.TransferValue +import org.prebid.cache.functional.service.ApiException +import org.prebid.cache.functional.util.getRandomString +import org.springframework.http.HttpStatus.UNAUTHORIZED + +class AuthenticationCacheSpec : ShouldSpec({ + + should("should save JSON transfer value without api-key in header when cache-write-secured is disabled") { + // given: Prebid Cache with api.cache-write-secured=false property + val prebidCacheApi = BaseSpec.getPrebidCacheApi( + prebidCacheConfig.getBaseRedisConfig( + allowExternalUuid = true, + cacheWriteSecured = false, + apiKey = getRandomString() + ) + ) + + // and: Request object with JSON transfer value + val requestObject = RequestObject.getDefaultJsonRequestObject() + val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java) + + // when: POST cache endpoint is called + val postResponse = prebidCacheApi.postCache(requestObject, apiKey = null) + + // when: GET cache endpoint is called + val getCacheResponse = BaseSpec.getPrebidCacheApi().getCache(postResponse.responses[0].uuid) + + // then: response content type is the same as request object type + getCacheResponse.contentType()?.contentType shouldBe "application" + getCacheResponse.contentType()?.contentSubtype shouldBe requestObject.puts[0].type.getValue() + + // and: transfer value is returned + val responseTransferValue = objectMapper.readValue(getCacheResponse.bodyAsText(), TransferValue::class.java) + + assertSoftly { + responseTransferValue.adm shouldBe requestTransferValue.adm + responseTransferValue.width shouldBe requestTransferValue.width + responseTransferValue.height shouldBe requestTransferValue.height + } + } + + should("should save JSON transfer value with proper api-key in header when cache-write-secured is enabled") { + // given: Prebid Cache with api.cache-write-secured=true property + val prebidApiKey = getRandomString() + val prebidCacheApi = BaseSpec.getPrebidCacheApi( + prebidCacheConfig.getBaseRedisConfig( + allowExternalUuid = true, + cacheWriteSecured = true, + apiKey = prebidApiKey + ) + ) + + // and: Request object with JSON transfer value + val requestObject = RequestObject.getDefaultJsonRequestObject() + val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java) + + // when: POST cache endpoint is called + val postResponse = prebidCacheApi.postCache(requestObject, apiKey = prebidApiKey) + + // when: GET cache endpoint is called + val getCacheResponse = BaseSpec.getPrebidCacheApi().getCache(postResponse.responses[0].uuid) + + // then: response content type is the same as request object type + getCacheResponse.contentType()?.contentType shouldBe "application" + getCacheResponse.contentType()?.contentSubtype shouldBe requestObject.puts[0].type.getValue() + + // and: transfer value is returned + val responseTransferValue = objectMapper.readValue(getCacheResponse.bodyAsText(), TransferValue::class.java) + + assertSoftly { + responseTransferValue.adm shouldBe requestTransferValue.adm + responseTransferValue.width shouldBe requestTransferValue.width + responseTransferValue.height shouldBe requestTransferValue.height + } + } + + should("should throw exception when cache-write-secured is enabled and trying to save payload transfer without api-key") { + // given: Prebid Cache with api.cache-write-secured=true property + val prebidCacheApi = BaseSpec.getPrebidCacheApi( + prebidCacheConfig.getBaseRedisConfig( + allowExternalUuid = true, + cacheWriteSecured = true, + apiKey = getRandomString() + ) + ) + + // and: Request object with JSON transfer value + val requestObject = RequestObject.getDefaultJsonRequestObject() + + // when: POST cache endpoint is called + val exception = shouldThrowExactly { prebidCacheApi.postCache(requestObject, apiKey = null) } + + // then: Bad Request exception is thrown + assertSoftly { + exception.statusCode shouldBe UNAUTHORIZED.value() + exception.responseBody should beEmpty() + } + } + + should("should throw exception when cache-write-secured is enabled and trying to save payload transfer with empty api-key") { + // given: Prebid Cache with api.cache-write-secured=true property + val prebidCacheApi = BaseSpec.getPrebidCacheApi( + prebidCacheConfig.getBaseRedisConfig( + allowExternalUuid = true, + cacheWriteSecured = true, + apiKey = getRandomString() + ) + ) + + // and: Request object with JSON transfer value + val requestObject = RequestObject.getDefaultJsonRequestObject() + + // when: POST cache endpoint is called + val exception = + shouldThrowExactly { prebidCacheApi.postCache(requestObject, apiKey = "") } + + // then: Bad Request exception is thrown + assertSoftly { + exception.statusCode shouldBe UNAUTHORIZED.value() + exception.responseBody should beEmpty() + } + } + + should("should throw exception when cache-write-secured is enabled and trying to save payload transfer with invalid api-key") { + // given: Prebid Cache with api.cache-write-secured=true property + val prebidCacheApi = BaseSpec.getPrebidCacheApi( + prebidCacheConfig.getBaseRedisConfig( + allowExternalUuid = true, + cacheWriteSecured = true, + apiKey = getRandomString() + ) + ) + + // and: Request object with JSON transfer value + val requestObject = RequestObject.getDefaultJsonRequestObject() + + // when: POST cache endpoint is called + val exception = + shouldThrowExactly { prebidCacheApi.postCache(requestObject, apiKey = getRandomString()) } + + // then: Bad Request exception is thrown + assertSoftly { + exception.statusCode shouldBe UNAUTHORIZED.value() + exception.responseBody should beEmpty() + } + } + + should("should throw exception when cache-write-secured is enabled and trying to save payload transfer with different case strategy api-key") { + // given: Prebid Cache with api.cache-write-secured=true property + val prebidApiKey = getRandomString() + val prebidCacheApi = BaseSpec.getPrebidCacheApi( + prebidCacheConfig.getBaseRedisConfig( + allowExternalUuid = true, + cacheWriteSecured = true, + apiKey = prebidApiKey + ) + ) + + // and: Request object with JSON transfer value + val requestObject = RequestObject.getDefaultJsonRequestObject() + + // when: POST cache endpoint is called + val exception = + shouldThrowExactly { prebidCacheApi.postCache(requestObject, apiKey = prebidApiKey.uppercase()) } + + // then: Bad Request exception is thrown + assertSoftly { + exception.statusCode shouldBe UNAUTHORIZED.value() + exception.responseBody should beEmpty() + } + } +}) diff --git a/src/test/kotlin/org/prebid/cache/functional/BaseSpec.kt b/src/test/kotlin/org/prebid/cache/functional/BaseSpec.kt index 1d3d23c..e6d083f 100644 --- a/src/test/kotlin/org/prebid/cache/functional/BaseSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/BaseSpec.kt @@ -13,7 +13,7 @@ abstract class BaseSpec { ContainerDependencies.apacheIgniteContainer.getContainerHost(), ) - fun getPrebidCacheApi(config: Map = prebidCacheConfig.getBaseRedisConfig("false")): PrebidCacheApi { + fun getPrebidCacheApi(config: Map = prebidCacheConfig.getBaseRedisConfig(false)): PrebidCacheApi { return ContainerDependencies.prebidCacheContainerPool.getPrebidCacheContainer(config) .let { container -> PrebidCacheApi(container.host, container.getHostPort()) } } diff --git a/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt index 9a99f0b..5f96717 100644 --- a/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/GeneralCacheSpec.kt @@ -8,6 +8,7 @@ import io.kotest.matchers.string.shouldContain import io.ktor.client.statement.bodyAsText import io.ktor.http.HttpStatusCode import io.ktor.http.contentType +import org.prebid.cache.functional.BaseSpec.Companion.prebidCacheConfig import org.prebid.cache.functional.mapper.objectMapper import org.prebid.cache.functional.model.request.MediaType.UNSUPPORTED import org.prebid.cache.functional.model.request.RequestObject @@ -48,7 +49,7 @@ class GeneralCacheSpec : ShouldSpec({ should("throw an exception when allow_external_UUID=true and payload transfer key not in UUID format is given") { // given: Prebid Cache with allow_external_UUID=true property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(BaseSpec.prebidCacheConfig.getBaseRedisConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig(true)) // and: Request object with set payload transfer key not in UUID format val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = getRandomUuid() + "*" } @@ -65,7 +66,7 @@ class GeneralCacheSpec : ShouldSpec({ should("throw an exception when allow_external_UUID=true and empty payload transfer key is given") { // given: Prebid Cache with allow_external_UUID=true property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(BaseSpec.prebidCacheConfig.getBaseRedisConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig(true)) // and: Request object with set empty payload transfer key val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = "" } @@ -99,12 +100,15 @@ class GeneralCacheSpec : ShouldSpec({ } should("return the same JSON transfer value which was saved to cache") { - // given: Request object with JSON transfer value + // given: Prebid Cache with routes.allow_public_write=true property + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig(true)) + + // and: Request object with JSON transfer value val requestObject = RequestObject.getDefaultJsonRequestObject() val requestTransferValue = objectMapper.readValue(requestObject.puts[0].value, TransferValue::class.java) // and: POST cache endpoint is called - val postResponse = BaseSpec.getPrebidCacheApi().postCache(requestObject) + val postResponse = prebidCacheApi.postCache(requestObject) // when: GET cache endpoint is called val getCacheResponse = BaseSpec.getPrebidCacheApi().getCache(postResponse.responses[0].uuid) diff --git a/src/test/kotlin/org/prebid/cache/functional/ProxyCacheHostSpec.kt b/src/test/kotlin/org/prebid/cache/functional/ProxyCacheHostSpec.kt index 829ce58..e00d176 100644 --- a/src/test/kotlin/org/prebid/cache/functional/ProxyCacheHostSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/ProxyCacheHostSpec.kt @@ -45,7 +45,7 @@ class ProxyCacheHostSpec : ShouldSpec({ // and: Prebid Cache with allow_external_UUID=true and configured proxy cache host is started proxyCacheHost = "${ContainerDependencies.webCacheContainer.getContainerHost()}:${WebCacheContainer.PORT}" - specPrebidCacheConfig = BaseSpec.prebidCacheConfig.getBaseRedisConfig("true") + + specPrebidCacheConfig = BaseSpec.prebidCacheConfig.getBaseRedisConfig(true) + BaseSpec.prebidCacheConfig.getProxyCacheHostConfig(proxyCacheHost) prebidCacheApi = BaseSpec.getPrebidCacheApi(specPrebidCacheConfig) } @@ -61,7 +61,7 @@ class ProxyCacheHostSpec : ShouldSpec({ should("throw an exception when proxy cache host doesn't exist") { // given: Prebid cache config with set up not existing proxy cache host val cacheHost = getRandomString() - val config = BaseSpec.prebidCacheConfig.getBaseRedisConfig("true") + + val config = BaseSpec.prebidCacheConfig.getBaseRedisConfig(true) + BaseSpec.prebidCacheConfig.getProxyCacheHostConfig(cacheHost) // when: GET cache endpoint is called with provided cache host diff --git a/src/test/kotlin/org/prebid/cache/functional/RedisCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/RedisCacheSpec.kt index 3feff70..d3d4401 100644 --- a/src/test/kotlin/org/prebid/cache/functional/RedisCacheSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/RedisCacheSpec.kt @@ -22,7 +22,7 @@ class RedisCacheSpec : ShouldSpec({ should("throw an exception when cache record is absent in Redis repository") { // given: Prebid cache config - val config = prebidCacheConfig.getBaseRedisConfig("true") + val config = prebidCacheConfig.getBaseRedisConfig(true) val cachePrefix = config["cache.prefix"] // when: GET cache endpoint with random UUID is called @@ -38,7 +38,7 @@ class RedisCacheSpec : ShouldSpec({ should("rethrow an exception from Redis cache server when such happens") { // given: Prebid Cache with set min and max expiry as 0 - val config = prebidCacheConfig.getBaseRedisConfig("false") + prebidCacheConfig.getCacheExpiryConfig("0", "0") + val config = prebidCacheConfig.getBaseRedisConfig(false) + prebidCacheConfig.getCacheExpiryConfig("0", "0") val prebidCacheApi = BaseSpec.getPrebidCacheApi(config) // and: Request object @@ -88,7 +88,7 @@ class RedisCacheSpec : ShouldSpec({ should("return back a request UUID when allow_external_UUID=true and request object was successfully cached") { // given: Prebid Cache with enabled allow_external_UUID property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig(true)) // and: Request object with set payload transfer UUID key val requestObject = RequestObject.getDefaultJsonRequestObject().apply { puts[0].key = getRandomUuid() } @@ -104,7 +104,7 @@ class RedisCacheSpec : ShouldSpec({ should("return back two request UUIDs when allow_external_UUID=true and 2 payload transfers were successfully cached in Redis") { // given: Prebid Cache with enabled allow_external_UUID property - val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig("true")) + val prebidCacheApi = BaseSpec.getPrebidCacheApi(prebidCacheConfig.getBaseRedisConfig(true)) // and: Request object with set 2 payload transfers val xmlPayloadTransfer = PayloadTransfer.getDefaultXmlPayloadTransfer().apply { key = getRandomUuid() } diff --git a/src/test/kotlin/org/prebid/cache/functional/SecondaryCacheSpec.kt b/src/test/kotlin/org/prebid/cache/functional/SecondaryCacheSpec.kt index 0f1e30f..a1aa745 100644 --- a/src/test/kotlin/org/prebid/cache/functional/SecondaryCacheSpec.kt +++ b/src/test/kotlin/org/prebid/cache/functional/SecondaryCacheSpec.kt @@ -31,7 +31,7 @@ class SecondaryCacheSpec : ShouldSpec({ "http://${ContainerDependencies.webCacheContainer.getContainerHost()}:${WebCacheContainer.PORT}" // and: Prebid Cache with allow_external_UUID=true and configured secondary cache is started - specPrebidCacheConfig = BaseSpec.prebidCacheConfig.getBaseAerospikeConfig("true") + + specPrebidCacheConfig = BaseSpec.prebidCacheConfig.getBaseAerospikeConfig(true) + BaseSpec.prebidCacheConfig.getSecondaryCacheConfig(webCacheContainerUri) prebidCacheApi = BaseSpec.getPrebidCacheApi(specPrebidCacheConfig) } diff --git a/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt b/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt index 59c6390..7cc55ff 100644 --- a/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt +++ b/src/test/kotlin/org/prebid/cache/functional/service/PrebidCacheApi.kt @@ -10,6 +10,7 @@ import io.ktor.client.plugins.defaultRequest import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.client.request.parameter +import io.ktor.client.request.port import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse @@ -22,13 +23,26 @@ import org.prebid.cache.functional.model.request.PayloadTransfer import org.prebid.cache.functional.model.request.RequestObject import org.prebid.cache.functional.model.response.ResponseObject -class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { +class PrebidCacheApi( + private val prebidCacheHost: String, + private val prebidCachePort: Int, +) { suspend fun getCache(uuid: String?, proxyCacheHost: String? = null): HttpResponse = - get(CACHE_ENDPOINT, mapOf(UUID_QUERY_PARAMETER to uuid, PROXY_CACHE_HOST_QUERY_PARAMETER to proxyCacheHost)) + get(endpoint = CACHE_ENDPOINT, + parameters = mapOf(UUID_QUERY_PARAMETER to uuid, PROXY_CACHE_HOST_QUERY_PARAMETER to proxyCacheHost)) - suspend fun postCache(requestObject: RequestObject, secondaryCache: String? = null): ResponseObject = - post(CACHE_ENDPOINT, requestObject, mapOf(SECONDARY_CACHE_QUERY_PARAMETER to secondaryCache)).body() + suspend fun postCache( + requestObject: RequestObject, + secondaryCache: String? = null, + apiKey: String? = null + ): ResponseObject = + post( + endpoint = CACHE_ENDPOINT, + requestObject = requestObject, + parameters = mapOf(SECONDARY_CACHE_QUERY_PARAMETER to secondaryCache), + headers = mapOf(API_KEY_PARAMETER to apiKey) + ).body() suspend fun getStorageCache( payloadTransferKey: String?, @@ -36,15 +50,15 @@ class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { apiKey: String? ): PayloadTransfer = get( - STORAGE_ENDPOINT, - mapOf(KEY_PARAMETER to payloadTransferKey, APPLICATION_PARAMETER to application), - mapOf(API_KEY_PARAMETER to apiKey) + endpoint = STORAGE_ENDPOINT, + parameters = mapOf(KEY_PARAMETER to payloadTransferKey, APPLICATION_PARAMETER to application), + headers = mapOf(API_KEY_PARAMETER to apiKey) ).body() suspend fun postStorageCache(requestObject: PayloadTransfer, apiKey: String? = null): Boolean = post( - STORAGE_ENDPOINT, - requestObject, + endpoint = STORAGE_ENDPOINT, + requestObject = requestObject, headers = mapOf(API_KEY_PARAMETER to apiKey) ).status == HttpStatusCode.NoContent @@ -52,7 +66,6 @@ class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { expectSuccess = true defaultRequest { host = prebidCacheHost - port = prebidCachePort header(ContentType, Json) } install(ContentNegotiation) { @@ -69,9 +82,11 @@ class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { private suspend fun get( endpoint: String, + requestPort: Int = prebidCachePort, parameters: Map = emptyMap(), headers: Map = emptyMap() ): HttpResponse = client.get(endpoint) { + port = requestPort parameters.forEach { (key, value) -> value?.let { parameter(key, value) } } @@ -82,11 +97,13 @@ class PrebidCacheApi(prebidCacheHost: String, prebidCachePort: Int) { private suspend fun post( endpoint: String, + requestPort: Int = prebidCachePort, requestObject: Any, parameters: Map = emptyMap(), headers: Map = emptyMap() ): HttpResponse = client.post(endpoint) { + port = requestPort setBody(requestObject) parameters.forEach { (key, value) -> value?.let { parameter(key, value) } diff --git a/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt b/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt index cab0ba8..8a3c79a 100644 --- a/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt +++ b/src/test/kotlin/org/prebid/cache/functional/testcontainers/PrebidCacheContainerConfig.kt @@ -7,21 +7,36 @@ import org.prebid.cache.functional.testcontainers.container.ApacheIgniteContaine import org.prebid.cache.functional.testcontainers.container.RedisContainer import org.prebid.cache.functional.testcontainers.container.WebCacheContainer.Companion.WEB_CACHE_PATH -class PrebidCacheContainerConfig(private val redisHost: String, - private val aerospikeHost: String, - private val apacheIgniteHost: String) { - - fun getBaseRedisConfig(allowExternalUuid: String): Map = - getBaseConfig(allowExternalUuid) + getRedisConfig() +class PrebidCacheContainerConfig( + private val redisHost: String, + private val aerospikeHost: String, + private val apacheIgniteHost: String +) { + + fun getBaseRedisConfig( + allowExternalUuid: Boolean, + cacheWriteSecured: Boolean = false, + apiKey: String? = null + ): Map = + getBaseConfig(allowExternalUuid, cacheWriteSecured, apiKey) + getRedisConfig() - fun getBaseAerospikeConfig(allowExternalUuid: String, aerospikeNamespace: String = NAMESPACE): Map = - getBaseConfig(allowExternalUuid) + getAerospikeConfig(aerospikeNamespace) + fun getBaseAerospikeConfig( + allowExternalUuid: Boolean, + aerospikeNamespace: String = NAMESPACE, + cacheWriteSecured: Boolean = false + ): Map = + getBaseConfig(allowExternalUuid, cacheWriteSecured) + getAerospikeConfig(aerospikeNamespace) - fun getBaseApacheIgniteConfig(allowExternalUuid: String, ingineCacheName: String = CACHE_NAME): Map = - getBaseConfig(allowExternalUuid) + getApacheIgniteConfig(ingineCacheName) + fun getBaseApacheIgniteConfig( + allowExternalUuid: Boolean, + ingineCacheName: String = CACHE_NAME, + cacheWriteSecured: Boolean = false + ): Map = + getBaseConfig(allowExternalUuid, cacheWriteSecured) + getApacheIgniteConfig(ingineCacheName) fun getBaseModuleStorageConfig(applicationName: String, apiKey: String): Map = - getBaseConfig("true") + getModuleStorageRedisConfig(apiKey, applicationName) + getRedisConfig() + getBaseConfig(allowExternalUuid = true, apiKey = apiKey) + + getModuleStorageRedisConfig(applicationName) + getRedisConfig() fun getCacheExpiryConfig(minExpiry: String = "15", maxExpiry: String = "28800"): Map = mapOf( @@ -33,8 +48,8 @@ class PrebidCacheContainerConfig(private val redisHost: String, fun getCacheTimeoutConfig(timeoutMs: String): Map = mapOf("cache.timeout.ms" to timeoutMs) - fun getAerospikePreventUuidDuplicationConfig(preventUuidDuplication: String): Map = - mapOf("spring.aerospike.prevent-u-u-i-d-duplication" to preventUuidDuplication) + fun getAerospikePreventUuidDuplicationConfig(preventUuidDuplication: Boolean): Map = + mapOf("spring.aerospike.prevent-u-u-i-d-duplication" to preventUuidDuplication.toString()) fun getSecondaryCacheConfig(secondaryCacheUri: String): Map = mapOf( @@ -75,26 +90,40 @@ class PrebidCacheContainerConfig(private val redisHost: String, ) private fun getModuleStorageRedisConfig( - apiKey: String, applicationName: String, timeoutMs: Long = 9999L, - endpoint: String = "/storage" ): Map = mapOf( - "api.api-key" to apiKey, - "api.storage-path" to endpoint, "storage.redis.${applicationName}.port" to RedisContainer.PORT.toString(), "storage.redis.${applicationName}.host" to redisHost, "storage.redis.${applicationName}.timeout" to timeoutMs.toString(), "storage.default-ttl-seconds" to 1000L.toString() ) - private fun getBaseConfig(allowExternalUuid: String): Map = - getCachePrefixConfig() + getCacheExpiryConfig() + getAllowExternalUuidConfig(allowExternalUuid) + - getCacheTimeoutConfig("2500") + private fun getBaseConfig( + allowExternalUuid: Boolean, + cacheWriteSecured: Boolean = false, + apiKey: String? = null, + endpoint: String = "/storage" + ): Map = + getCachePrefixConfig() + + getCacheExpiryConfig() + + getAllowExternalUuidConfig(allowExternalUuid) + + getCacheTimeoutConfig("2500") + + getApiConfig(endpoint, apiKey, cacheWriteSecured) private fun getCachePrefixConfig(): Map = mapOf("cache.prefix" to "prebid_") - private fun getAllowExternalUuidConfig(allowExternalUuid: String): Map = - mapOf("cache.allow-external-u-u-i-d" to allowExternalUuid) + private fun getAllowExternalUuidConfig(allowExternalUuid: Boolean): Map = + mapOf("cache.allow-external-u-u-i-d" to allowExternalUuid.toString()) + + private fun getApiConfig( + endpoint: String, + apiKey: String? = null, + allowPublicWrite: Boolean? = null + ): Map = buildMap { + put("api.storage-path", endpoint) + apiKey?.let { put("api.api-key", it) } + allowPublicWrite?.let { put("api.cache-write-secured", it.toString()) } + } }