Skip to content

Commit

Permalink
fix(datadog): moved datadog credentials from query params to headers …
Browse files Browse the repository at this point in the history
…so that they are not logged (#725)

* fix(datadog): moved datadog credentials from query params to headers so that they are not logged

* fix: update copyright header
  • Loading branch information
fieldju committed May 8, 2020
1 parent b0dd988 commit 13bea2f
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static retrofit.Endpoints.newFixedEndpoint;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spinnaker.kork.annotations.VisibleForTesting;
import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger;
import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.Credentials;
Expand All @@ -29,6 +30,7 @@
import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -44,7 +46,11 @@
public class RetrofitClientFactory {

@Value("${retrofit.log-level:BASIC}")
String retrofitLogLevel;
@VisibleForTesting
public String retrofitLogLevel;

@VisibleForTesting
public Function<Class<?>, Slf4jRetrofitLogger> createRetrofitLogger = Slf4jRetrofitLogger::new;

@Bean
JacksonConverter jacksonConverterWithMapper(ObjectMapper objectMapper) {
Expand Down Expand Up @@ -81,12 +87,14 @@ public <T> T createClient(
okHttpClient = createAuthenticatedClient(username, password, usernamePasswordFile);
}

Slf4jRetrofitLogger logger = createRetrofitLogger.apply(type);

return new RestAdapter.Builder()
.setEndpoint(endpoint)
.setClient(new OkClient(okHttpClient))
.setConverter(converter)
.setLogLevel(RestAdapter.LogLevel.valueOf(retrofitLogLevel))
.setLog(new Slf4jRetrofitLogger(type))
.setLog(logger)
.build()
.create(type);
}
Expand Down
3 changes: 3 additions & 0 deletions kayenta-datadog/kayenta-datadog.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
dependencies {
implementation project(":kayenta-core")

testImplementation group: 'junit', name: 'junit'
testImplementation group: 'org.mock-server', name: 'mockserver-junit-rule', version: '5.10.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
import com.netflix.kayenta.datadog.security.DatadogNamedAccountCredentials;
import com.netflix.kayenta.datadog.service.DatadogRemoteService;
import com.netflix.kayenta.metrics.MetricsService;
import com.netflix.kayenta.retrofit.config.RemoteService;
import com.netflix.kayenta.retrofit.config.RetrofitClientFactory;
import com.netflix.kayenta.security.AccountCredentials;
import com.netflix.kayenta.security.AccountCredentialsRepository;
import com.netflix.spinnaker.kork.annotations.VisibleForTesting;
import com.squareup.okhttp.OkHttpClient;
import java.io.IOException;
import java.util.List;
Expand Down Expand Up @@ -86,11 +88,8 @@ MetricsService datadogMetricsService(
if (!CollectionUtils.isEmpty(supportedTypes)) {
if (supportedTypes.contains(AccountCredentials.Type.METRICS_STORE)) {
accountCredentialsBuilder.datadogRemoteService(
retrofitClientFactory.createClient(
DatadogRemoteService.class,
new JacksonConverter(objectMapper),
account.getEndpoint(),
okHttpClient));
createDatadogRemoteService(
retrofitClientFactory, objectMapper, account.getEndpoint(), okHttpClient));
}
accountCredentialsBuilder.supportedTypes(supportedTypes);
}
Expand All @@ -104,4 +103,15 @@ MetricsService datadogMetricsService(
datadogConfigurationProperties.getAccounts().size());
return metricsServiceBuilder.build();
}

@VisibleForTesting
public static DatadogRemoteService createDatadogRemoteService(
RetrofitClientFactory retrofitClientFactory,
ObjectMapper objectMapper,
RemoteService endpoint,
OkHttpClient okHttpClient) {

return retrofitClientFactory.createClient(
DatadogRemoteService.class, new JacksonConverter(objectMapper), endpoint, okHttpClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@

import com.netflix.kayenta.model.DatadogMetricDescriptorsResponse;
import retrofit.http.GET;
import retrofit.http.Header;
import retrofit.http.Query;

public interface DatadogRemoteService {

// See https://docs.datadoghq.com/api/?lang=python#query-time-series-points
@GET("/api/v1/query")
DatadogTimeSeries getTimeSeries(
@Query("api_key") String apiKey,
@Query("application_key") String applicationKey,
@Header("DD-API-KEY") String apiKey,
@Header("DD-APPLICATION-KEY") String applicationKey,
@Query("from") int startTimestamp,
@Query("to") int endTimestamp,
@Query("query") String query);

@GET("/api/v1/metrics")
DatadogMetricDescriptorsResponse getMetrics(
@Query("api_key") String apiKey,
@Query("application_key") String applicationKey,
@Header("DD-API-KEY") String apiKey,
@Header("DD-APPLICATION-KEY") String applicationKey,
@Query("from") long from);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright 2020 Armory, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.netflix.kayenta.datadog.functional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
import com.netflix.kayenta.datadog.config.DatadogConfiguration;
import com.netflix.kayenta.datadog.service.DatadogRemoteService;
import com.netflix.kayenta.datadog.service.DatadogTimeSeries;
import com.netflix.kayenta.model.DatadogMetricDescriptorsResponse;
import com.netflix.kayenta.retrofit.config.RemoteService;
import com.netflix.kayenta.retrofit.config.RetrofitClientFactory;
import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger;
import com.squareup.okhttp.OkHttpClient;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.junit.MockServerRule;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
import org.slf4j.LoggerFactory;

/** TDD for https://github.com/spinnaker/kayenta/issues/684 */
public class DatadogSecretsDoNotLeakWhenApiCalledFunctionalTest {

private static final String API_KEY = "IAMASECRETANDSHOULDNOTBELOGGED";
private static final String APPLICATION_KEY = "IAMASECRETANDALSOSHOULDNOTBELOGGED";
private static final List<String> SECRETS = ImmutableList.of(API_KEY, APPLICATION_KEY);

@Rule public MockServerRule mockServerRule = new MockServerRule(this);

private ListAppender<ILoggingEvent> listAppender;

private MockServerClient mockServerClient;
private ObjectMapper objectMapper;

DatadogRemoteService datadogRemoteService;

@Before
public void before() {
listAppender = new ListAppender<>();
Logger mockLogger =
(Logger) LoggerFactory.getLogger("DatadogSecretsDoNotLeakWhenApiCalledFunctionalTest");
mockLogger.addAppender(listAppender);
listAppender.start();

RetrofitClientFactory retrofitClientFactory = new RetrofitClientFactory();
retrofitClientFactory.retrofitLogLevel = "BASIC";
retrofitClientFactory.createRetrofitLogger =
(type) -> {
return new Slf4jRetrofitLogger(mockLogger);
};

objectMapper = new ObjectMapper();
datadogRemoteService =
DatadogConfiguration.createDatadogRemoteService(
retrofitClientFactory,
objectMapper,
new RemoteService().setBaseUrl("http://localhost:" + mockServerRule.getPort()),
new OkHttpClient());
}

@Test
public void
test_that_the_datadog_remote_service_does_not_log_the_api_key_when_getTimeSeries_is_called() {
DatadogTimeSeries mockResponse = new DatadogTimeSeries();
String mockResponseAsString;
try {
mockResponseAsString = objectMapper.writeValueAsString(mockResponse);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize mock DatadogTimeSeries response");
}
mockServerClient
.when(HttpRequest.request())
.respond(HttpResponse.response(mockResponseAsString));

Instant now = Instant.now();
DatadogTimeSeries res =
datadogRemoteService.getTimeSeries(
API_KEY,
APPLICATION_KEY,
(int) now.minus(5, ChronoUnit.MINUTES).toEpochMilli(),
(int) now.toEpochMilli(),
"some query");

assertEquals(mockResponse, res);
assertTrue("We expected there to be at least 1 logged message", listAppender.list.size() > 0);
assertMessagesDoNotContainSecrets(listAppender.list);
}

@Test
public void
test_that_the_datadog_remote_service_does_not_log_the_api_key_when_getMetrics_is_called() {
DatadogMetricDescriptorsResponse mockResponse = new DatadogMetricDescriptorsResponse();
String mockResponseAsString;
try {
mockResponseAsString = objectMapper.writeValueAsString(mockResponse);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize mock DatadogTimeSeries response");
}

mockServerClient
.when(HttpRequest.request())
.respond(HttpResponse.response(mockResponseAsString));

DatadogMetricDescriptorsResponse res =
datadogRemoteService.getMetrics(
API_KEY, APPLICATION_KEY, Instant.now().minus(5, ChronoUnit.MINUTES).toEpochMilli());

assertEquals(mockResponse, res);
assertTrue("We expected there to be at least 1 logged message", listAppender.list.size() > 0);
assertMessagesDoNotContainSecrets(listAppender.list);
}

private void assertMessagesDoNotContainSecrets(List<ILoggingEvent> events) {
List<String> matchedLogStatements =
events.stream()
.filter(
event -> SECRETS.stream().anyMatch(secret -> event.getMessage().contains(secret)))
.map(ILoggingEvent::getMessage)
.collect(Collectors.toList());

if (matchedLogStatements.size() > 0) {
StringBuilder msg =
new StringBuilder(
"The log messages should not contain secrets, but the following messages had secrets");
matchedLogStatements.forEach(m -> msg.append('\n').append(m));
fail(msg.toString());
}
}
}

0 comments on commit 13bea2f

Please sign in to comment.