Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to add auth headers to Druid requests. #62

Merged
merged 1 commit into from
Oct 18, 2016
Merged

Conversation

codingwhatever
Copy link
Collaborator

We are adding security to our Druid cluster and need Fili to be able to send a token in a header for authenticating our Druid requests.

Copy link
Collaborator

@cdeszaq cdeszaq left a comment

Choose a reason for hiding this comment

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

Just a few small things. Overall, looks pretty good

@@ -100,6 +100,7 @@
import com.codahale.metrics.health.HealthCheckRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.apache.commons.collections.map.HashedMap;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use a built-in (ie. Java Collections) map instead of apache commons?

public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper) {
this(serviceConfig, initializeWebClient(serviceConfig.getTimeout()), mapper);
public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper,
Supplier<Map<String, String>> authHeaders
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wrapping param lists should "chop" the list, rather than just wrap it. eg.:

public AsyncDruidWebServiceImpl(
        DruidServiceConfig serviceConfig, 
        ObjectMapper mapper,
        Supplier<Map<String, String>> authHeaders
) {

@@ -70,8 +72,10 @@
* @param serviceConfig Configuration for the Druid Service
* @param mapper A shared jackson object mapper resource
Copy link
Collaborator

Choose a reason for hiding this comment

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

Javadoc needed for the new parameter

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed this in the constructor below.

public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper) {
this(serviceConfig, initializeWebClient(serviceConfig.getTimeout()), mapper);
public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper,
Supplier<Map<String, String>> authHeaders
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we re-name this? Rather than auth-specific, can this be named so that it can be any headers?

@@ -102,7 +106,9 @@ private static AsyncHttpClient initializeWebClient(int requestTimeout) {
* @param asyncHttpClient the HTTP client
* @param mapper A shared jackson object mapper resource
*/
public AsyncDruidWebServiceImpl(DruidServiceConfig config, AsyncHttpClient asyncHttpClient, ObjectMapper mapper) {
public AsyncDruidWebServiceImpl(DruidServiceConfig config, AsyncHttpClient asyncHttpClient, ObjectMapper mapper,
Supplier<Map<String, String>> authHeaders
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same comments re: wrapping and name apply here as well

@@ -61,7 +63,7 @@
public static final String DRUID_WEIGHTED_QUERY_TIMER = DRUID_TIMER + "_W_";
public static final String DRUID_SEGMENT_METADATA_TIMER = DRUID_TIMER + "_S_0";


private final Supplier<Map<String, String>> authHeaders;
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we rename this to indicate any headers?

@@ -102,7 +106,9 @@ private static AsyncHttpClient initializeWebClient(int requestTimeout) {
* @param asyncHttpClient the HTTP client
* @param mapper A shared jackson object mapper resource
*/
public AsyncDruidWebServiceImpl(DruidServiceConfig config, AsyncHttpClient asyncHttpClient, ObjectMapper mapper) {
public AsyncDruidWebServiceImpl(DruidServiceConfig config, AsyncHttpClient asyncHttpClient, ObjectMapper mapper,
Supplier<Map<String, String>> authHeaders
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should also have constructors that default the header supplier for backwards compatibility. We can deprecate them so that people are required to supply a supplier eventually, but we should try to maintain backwards compatibility if we can.

The supplier can just return an empty map in the defaulting case.

@codingwhatever
Copy link
Collaborator Author

@cdeszaq Addressed all the comments.

@@ -72,10 +73,27 @@
* @param serviceConfig Configuration for the Druid Service
* @param mapper A shared jackson object mapper resource
*/
public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper,
Supplier<Map<String, String>> authHeaders
@Deprecated
Copy link
Contributor

Choose a reason for hiding this comment

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

If you could add the @deprecated annotation to the JavaDoc as well, with a brief explanation of why this constructor is being deprecated, and which constructor should be used instead, I would be grateful.

@@ -70,8 +73,27 @@
* @param serviceConfig Configuration for the Druid Service
* @param mapper A shared jackson object mapper resource
*/
public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper) {
this(serviceConfig, initializeWebClient(serviceConfig.getTimeout()), mapper);
@Deprecated
Copy link
Contributor

Choose a reason for hiding this comment

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

The @deprecated annotation should also be added to the JavaDoc, with a brief explanation of why this is being deprecated, and a pointer to which constructor should be used instead.

@@ -102,7 +124,29 @@ private static AsyncHttpClient initializeWebClient(int requestTimeout) {
* @param asyncHttpClient the HTTP client
* @param mapper A shared jackson object mapper resource
*/
public AsyncDruidWebServiceImpl(DruidServiceConfig config, AsyncHttpClient asyncHttpClient, ObjectMapper mapper) {
@Deprecated
Copy link
Contributor

Choose a reason for hiding this comment

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

@archolewa
Copy link
Contributor

This also needs a CHANGELOG entry.

Copy link
Collaborator

@cdeszaq cdeszaq left a comment

Choose a reason for hiding this comment

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

Other than the minor JavaDoc update on the 2 deprecated constructors, and adding entries to the ChangeLog file (a bit in the ### Deprecated section and a bit in the ### Changed section), this PR looks like it's very close!

@@ -70,8 +73,27 @@
* @param serviceConfig Configuration for the Druid Service
* @param mapper A shared jackson object mapper resource
*/
public AsyncDruidWebServiceImpl(DruidServiceConfig serviceConfig, ObjectMapper mapper) {
this(serviceConfig, initializeWebClient(serviceConfig.getTimeout()), mapper);
@Deprecated
Copy link
Collaborator

Choose a reason for hiding this comment

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

For these deprecations, we should add an @deprecated tag to the javadoc indicating why we're deprecating them (because you should specify your header supplier), and what you should use instead (the matching constructor with that new parameter)

Applies to the other @Deprecated instance in this PR as well

Copy link
Collaborator

@cdeszaq cdeszaq left a comment

Choose a reason for hiding this comment

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

Just a few thoughts around the extension hook for the Supplier, but otherwise that extension looks good.

PR still needs CHANGELOG entries and updates the the JavaDoc for those @Deprecated methods documenting the deprecation

@@ -919,7 +924,24 @@ protected JobPayloadBuilder buildJobPayloadBuilder() {
* @return A DruidWebService
*/
protected DruidWebService buildDruidWebService(DruidServiceConfig druidServiceConfig, ObjectMapper mapper) {
return new AsyncDruidWebServiceImpl(druidServiceConfig, mapper, HashMap::new);
Supplier<Map<String, String>> supplier = HashMap::new;
String customSupplierClassString = SYSTEM_CONFIG.getStringProperty(DRUID_HEADER_SUPPLIER_CLASS, null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

@michael-mclawhorn does this usage fit with how we usually use systemConfig?

Constructor<?> constructor = c.getConstructor();
supplier = (Supplier<Map<String, String>>) constructor.newInstance();
} catch (ClassNotFoundException | NoSuchMethodException |
IllegalAccessException | InstantiationException | InvocationTargetException e) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rather than enumerate all of these, should we just catch Exception? If anything goes wrong loading this, we should just fail anyway, no matter what the exception is, right?

);
throw new IllegalStateException(e);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should pull the buildDruidWebServiceHeaderSupplier code into it's own method. That will give another (fairly easy) override-based config mechanism if someone's Supplier needs more than a no-param constructor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I assumed we could just pull the other params from system_config in the supplier. I guess there could be more complicated use cases?

@codingwhatever codingwhatever force-pushed the auth-headers branch 4 times, most recently from d2f5179 to fcdad74 Compare October 13, 2016 18:19
@codingwhatever
Copy link
Collaborator Author

@archolewa @cdeszaq Addressed all review comments.

Copy link
Collaborator

@cdeszaq cdeszaq left a comment

Choose a reason for hiding this comment

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

👍

@michael-mclawhorn
Copy link
Contributor

michael-mclawhorn commented Oct 13, 2016

I think Supplier<Map<String, String>> might be inferior as the type here to Iterable<Pair<String, String>>

Copy link
Contributor

@michael-mclawhorn michael-mclawhorn left a comment

Choose a reason for hiding this comment

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

I think the Supplier<Map<S,S>> type should be seriously reconsidered.

A better name for the additional headers and documentation of the semantics (adding vs replacing) on the request would be a good idea.

Also, tests.

At minimum, postDruidQuery should be tested to verify that the added headers get added.
Abstract binder factory being tested with actually building a header source from a config parameter and an empty one from null would be really good.

Alternatives to Supplier would be an Iterable or an rx Observable.

@@ -61,17 +64,38 @@
public static final String DRUID_WEIGHTED_QUERY_TIMER = DRUID_TIMER + "_W_";
public static final String DRUID_SEGMENT_METADATA_TIMER = DRUID_TIMER + "_S_0";


private final Supplier<Map<String, String>> anyHeaders;
private final DruidServiceConfig serviceConfig;
Copy link
Contributor

Choose a reason for hiding this comment

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

I dislike the name of the variable. If I understand the scope of this, these are mandatory headers appended (or possibly prepended, it looks like the latter based on digging into the netty code) to the client request. So possible 'requestAppendHeaders' or 'clientAppendHeaders'.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we're adding the headers (and it looks like added headers strictly add, don't replace), I think a name like headersToAdd or headersToAppend helps clarify this.

.addHeader("Content-Type", "application/json");

anyHeaders.get().forEach((k, v) -> requestBuilder.addHeader(k, v));

Copy link
Contributor

Choose a reason for hiding this comment

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

It does not appear that headers are maplike in that there can't be more than one of a given key value. Given this, you might be better served with an iterable here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

While the AsyncHttpClient allows for multi-value headers, I think it's OK for us to restrict it down to just being able to add single-valued headers. We can expand that later if we need to.

Constructor<?> constructor = c.getConstructor();
supplier = (Supplier<Map<String, String>>) constructor.newInstance();
} catch (Exception e) {
LOG.error(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we catch something more specific than 'Exception' here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We were matching something more specific in an earlier version of this PR and I suggested replacing that list of roughly 5 exceptions with just Exception. Since we're re-throwing the exception anyways, catching everything so that we can log it (since it'll be fatal anyways) is (I think) fine, and lets the code be smaller / cleaner.

.setBody(entityBody)
.addHeader("Content-Type", "application/json");

anyHeaders.get().forEach((k, v) -> requestBuilder.addHeader(k, v));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can be replaced with method reference (requestBuilder::addHeader)

@michael-mclawhorn
Copy link
Contributor

To summarize my responses, since I distributed them across Rick's responses to my comments:
The only change I am currently asking for is the anyHeaders variable to be named slightly differently. Otherwise 👍 from me.

Copy link
Collaborator

@cdeszaq cdeszaq left a comment

Choose a reason for hiding this comment

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

Definitely need to test that we're adding any supplied headers on the Druid requests.

I don't know if we have a hook for that in our testing framework yet, but I don't know if there's a need for such a hook in the functional tests (it would be OK to have one). If we don't want to add one, I think we should be able to test the Async client in isolation.

To do so, we may need to either extend an existing or add a new TestServlet (which are the pieces we use to capture requests to "druid")

@codingwhatever
Copy link
Collaborator Author

@cdeszaq Addressed remaining comments and added a unit test.

Copy link
Collaborator

@cdeszaq cdeszaq left a comment

Choose a reason for hiding this comment

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

Very close, just a few minor things!

@@ -0,0 +1,53 @@
package com.yahoo.bard.webservice.druid.client.impl
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copyright needed

DruidClientConfigHelper.getNonUiServiceConfig(),
new ObjectMapper(),
supplier
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

All of this stuff from here up that's in the when block should go up in the setup block, since it's still setting up the scenario.

If needed, you can use and: blocks (with an optional comment: and: "a set of expected headers to add") to break up any block.

then:
HttpHeaders actualHeaders = webServiceImplWrapper.getHeaders()
for (Map.Entry<String, String> header : expectedHeaders) {
actualHeaders.get(header.getKey()) == header.getValue()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Only top-level expressions in a then block are treated as assertions. To treat non-top-level expressions as assertions, you need to explicitly add the assert keyword: assert actualHeaders.get(header.getKey()) == header.getValue()

protected Supplier<Map<String, String>> buildDruidWebServiceHeaderSupplier() {
Supplier<Map<String, String>> supplier = HashMap::new;
String customSupplierClassString = SYSTEM_CONFIG.getStringProperty(DRUID_HEADER_SUPPLIER_CLASS, null);
if (customSupplierClassString != null && customSupplierClassString != "") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should fix this too.

@codingwhatever
Copy link
Collaborator Author

@cdeszaq Addressed the latest comments.

@cdeszaq
Copy link
Collaborator

cdeszaq commented Oct 18, 2016

👍 just needs to be squashed and rebased onto master and I think this is good to merge.

@cdeszaq cdeszaq merged commit b195343 into master Oct 18, 2016
@cdeszaq cdeszaq deleted the auth-headers branch October 20, 2016 19:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants