Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<awaitility.version>4.2.0</awaitility.version>
<resilience4j.version>2.1.0</resilience4j.version>
<log4j2.version>2.20.0</log4j2.version>
<commons-collections4.version>4.6.0</commons-collections4.version>

<!-- Functional Tests -->
<kotest.version>5.9.1</kotest.version>
Expand Down Expand Up @@ -150,6 +151,11 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons-collections4.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PayloadWrapper, String> repository;
private final CacheConfig config;
Expand All @@ -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<String, String> userKeys;

public PostCacheHandler(final ReactiveRepository<PayloadWrapper, String> repository,
final CacheConfig config,
Expand Down Expand Up @@ -95,15 +99,14 @@ public PostCacheHandler(final ReactiveRepository<PayloadWrapper, String> reposit
this.metricTagPrefix = "write";
this.circuitBreaker = webClientCircuitBreaker;
this.apiConfig = apiConfig;
this.userKeys = MapUtils.invertMap(MapUtils.emptyIfNull(apiConfig.getUserKeys()));
}

public Mono<ServerResponse> 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();
}
Expand Down Expand Up @@ -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));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        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<PayloadTransfer, PayloadWrapper> payloadWrapperTransformer() {
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/prebid/cache/metrics/MeasurementTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/prebid/cache/metrics/MetricsRecorder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/prebid/cache/routers/ApiConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;

import java.util.Map;

@Data
@NoArgsConstructor
@AllArgsConstructor
Expand All @@ -28,6 +30,8 @@ public class ApiConfig {
@NotEmpty
private String apiKey;

private Map<String, String> userKeys;

// workaround for supporting transition period of deprecated path property
public void setPath(String path) {
cachePath = path;
Expand Down
115 changes: 115 additions & 0 deletions src/test/java/org/prebid/cache/handlers/PostCacheHandlerTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
}
}
Loading