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

HTTP Basic authentication on the HTTP source plugin via plugins #545

Merged
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ private <T> PluginArgumentsContext getConstructionContext(final PluginSetting pl
return new PluginArgumentsContext.Builder()
.withPluginSetting(pluginSetting)
.withPluginConfiguration(configuration)
.withPluginFactory(this)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.amazon.dataprepper.metrics.PluginMetrics;
import com.amazon.dataprepper.model.configuration.PluginSetting;
import com.amazon.dataprepper.model.plugin.InvalidPluginDefinitionException;
import com.amazon.dataprepper.model.plugin.PluginFactory;

import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -30,6 +31,9 @@ private PluginArgumentsContext(final Builder builder) {
}

typedArgumentsSuppliers.put(PluginMetrics.class, () -> PluginMetrics.fromPluginSetting(builder.pluginSetting));

if(builder.pluginFactory != null)
typedArgumentsSuppliers.put(PluginFactory.class, () -> builder.pluginFactory);
}

Object[] createArguments(final Class<?>[] parameterTypes) {
Expand All @@ -50,6 +54,7 @@ private Supplier<Object> getRequiredArgumentSupplier(final Class<?> parameterTyp
static class Builder {
private Object pluginConfiguration;
private PluginSetting pluginSetting;
private PluginFactory pluginFactory;

Builder withPluginConfiguration(final Object pluginConfiguration) {
this.pluginConfiguration = pluginConfiguration;
Expand All @@ -61,6 +66,11 @@ Builder withPluginSetting(final PluginSetting pluginSetting) {
return this;
}

Builder withPluginFactory(final PluginFactory pluginFactory) {
this.pluginFactory = pluginFactory;
return this;
}

PluginArgumentsContext build() {
return new PluginArgumentsContext(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.amazon.dataprepper.metrics.PluginMetrics;
import com.amazon.dataprepper.model.configuration.PluginSetting;
import com.amazon.dataprepper.model.plugin.InvalidPluginDefinitionException;
import com.amazon.dataprepper.model.plugin.PluginFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
Expand Down Expand Up @@ -70,6 +71,18 @@ void createArguments_with_two_classes_inverted_order() {
equalTo(new Object[] { pluginSetting, testPluginConfiguration }));
}

@Test
void createArguments_with_pluginFactory_should_return_the_instance_from_the_builder() {
final PluginFactory pluginFactory = mock(PluginFactory.class);
final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder()
.withPluginSetting(pluginSetting)
.withPluginFactory(pluginFactory)
.build();

assertThat(objectUnderTest.createArguments(new Class[] { PluginFactory.class }),
equalTo(new Object[] { pluginFactory }));
}

@Test
void createArguments_with_PluginMetrics() {
final PluginArgumentsContext objectUnderTest = new PluginArgumentsContext.Builder()
Expand Down
6 changes: 6 additions & 0 deletions data-prepper-plugins/armeria-common/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

dependencies {
implementation project(':data-prepper-api')
implementation 'com.linecorp.armeria:armeria:1.9.2'
testImplementation 'com.linecorp.armeria:armeria-junit5:1.9.2'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.amazon.dataprepper.armeria.authentication;

import com.linecorp.armeria.server.ServerBuilder;

/**
* An interface for providing authentication in Armeria-based HTTP servers.
* <p>
* Plugin authors can use this interface for Armeria authentication in
* HTTP servers.
*
* @since 1.2
*/
public interface ArmeriaAuthenticationProvider {
/**
* The plugin name for the plugin which allows unauthenticated
* requests. This plugin will disable authentication.
*/
String UNAUTHENTICATED_PLUGIN_NAME = "unauthenticated";

/**
* Adds an authentication decorator to an Armeria {@link ServerBuilder}.
*
* @param serverBuilder the builder for the server needing authentication
* @since 1.2
*/
void addAuthenticationDecorator(ServerBuilder serverBuilder);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.amazon.dataprepper.armeria.authentication;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Configuration for HTTP Basic Authentication.
*
* @since 1.2
*/
public class HttpBasicAuthenticationConfig {
private final String username;
private final String password;

@JsonCreator
public HttpBasicAuthenticationConfig(
@JsonProperty("username") final String username,
@JsonProperty("password") final String password) {
this.username = username;
this.password = password;
}

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.amazon.dataprepper.plugins;

import com.amazon.dataprepper.armeria.authentication.ArmeriaAuthenticationProvider;
import com.amazon.dataprepper.armeria.authentication.HttpBasicAuthenticationConfig;
import com.amazon.dataprepper.model.annotations.DataPrepperPlugin;
import com.amazon.dataprepper.model.annotations.DataPrepperPluginConstructor;
import com.linecorp.armeria.server.HttpService;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.auth.AuthService;

import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

/**
* The plugin for HTTP Basic authentication of Armeria servers.
*
* @since 1.2
*/
@DataPrepperPlugin(name = "http_basic",
pluginType = ArmeriaAuthenticationProvider.class,
pluginConfigurationType = HttpBasicAuthenticationConfig.class)
public class HttpBasicArmeriaAuthenticationProvider implements ArmeriaAuthenticationProvider {

private final HttpBasicAuthenticationConfig httpBasicAuthenticationConfig;

@DataPrepperPluginConstructor
public HttpBasicArmeriaAuthenticationProvider(final HttpBasicAuthenticationConfig httpBasicAuthenticationConfig) {
Objects.requireNonNull(httpBasicAuthenticationConfig);
Objects.requireNonNull(httpBasicAuthenticationConfig.getUsername());
Objects.requireNonNull(httpBasicAuthenticationConfig.getPassword());
this.httpBasicAuthenticationConfig = httpBasicAuthenticationConfig;
}

@Override
public void addAuthenticationDecorator(final ServerBuilder serverBuilder) {
serverBuilder.decorator(createDecorator());
}

private Function<? super HttpService, ? extends HttpService> createDecorator() {
return AuthService.builder()
.addBasicAuth((context, basic) ->
CompletableFuture.completedFuture(
httpBasicAuthenticationConfig.getUsername().equals(basic.username()) &&
httpBasicAuthenticationConfig.getPassword().equals(basic.password())))
.newDecorator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.amazon.dataprepper.plugins;

import com.amazon.dataprepper.armeria.authentication.ArmeriaAuthenticationProvider;
import com.amazon.dataprepper.model.annotations.DataPrepperPlugin;
import com.linecorp.armeria.server.ServerBuilder;

/**
* The plugin to use for unauthenticated access to Armeria servers. It
* disables authentication on endpoints.
*
* @since 1.2
*/
@DataPrepperPlugin(name = ArmeriaAuthenticationProvider.UNAUTHENTICATED_PLUGIN_NAME, pluginType = ArmeriaAuthenticationProvider.class)
public class UnauthenticatedArmeriaAuthenticationProvider implements ArmeriaAuthenticationProvider {
@Override
public void addAuthenticationDecorator(final ServerBuilder serverBuilder) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.amazon.dataprepper.plugins;

import com.amazon.dataprepper.armeria.authentication.HttpBasicAuthenticationConfig;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.auth.BasicToken;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.util.UUID;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;

class HttpBasicArmeriaAuthenticationProviderTest {

private static final String USERNAME = UUID.randomUUID().toString();
private static final String PASSWORD = UUID.randomUUID().toString();

@RegisterExtension
static ServerExtension server = new ServerExtension() {
@Override
protected void configure(final ServerBuilder sb) {
sb.service("/test", (ctx, req) -> HttpResponse.of(200));

final HttpBasicAuthenticationConfig config = mock(HttpBasicAuthenticationConfig.class);
when(config.getUsername()).thenReturn(USERNAME);
when(config.getPassword()).thenReturn(PASSWORD);
new HttpBasicArmeriaAuthenticationProvider(config).addAuthenticationDecorator(sb);
}
};

@Nested
class ConstructorTests {
private HttpBasicAuthenticationConfig config;

@BeforeEach
void setUp() {
config = mock(HttpBasicAuthenticationConfig.class);

}

private HttpBasicArmeriaAuthenticationProvider createObjectUnderTest() {
return new HttpBasicArmeriaAuthenticationProvider(config);
}

@Test
void constructor_with_null_Config_throws() {
config = null;
assertThrows(NullPointerException.class, this::createObjectUnderTest);
}

@Test
void constructor_with_null_username_throws() {
reset(config);
when(config.getPassword()).thenReturn(UUID.randomUUID().toString());
assertThrows(NullPointerException.class, this::createObjectUnderTest);
}

@Test
void constructor_with_null_password_throws() {
reset(config);
when(config.getUsername()).thenReturn(UUID.randomUUID().toString());
assertThrows(NullPointerException.class, this::createObjectUnderTest);
}
}

@Nested
class WithServer {
@Test
void httpRequest_without_authentication_responds_Unauthorized() {
final WebClient client = WebClient.of(server.httpUri());

final AggregatedHttpResponse httpResponse = client.get("/test").aggregate().join();

assertThat(httpResponse.status(), equalTo(HttpStatus.UNAUTHORIZED));
}

@Test
void httpRequest_with_incorrect_authentication_responds_Unauthorized() {
final WebClient client = WebClient.builder(server.httpUri())
.auth(BasicToken.of(UUID.randomUUID().toString(), UUID.randomUUID().toString()))
.build();

final AggregatedHttpResponse httpResponse = client.get("/test").aggregate().join();

assertThat(httpResponse.status(), equalTo(HttpStatus.UNAUTHORIZED));
}

@Test
void httpRequest_with_correct_authentication_responds_OK() {
final WebClient client = WebClient.builder(server.httpUri())
.auth(BasicToken.of(USERNAME, PASSWORD))
.build();

final AggregatedHttpResponse httpResponse = client.get("/test").aggregate().join();

assertThat(httpResponse.status(), equalTo(HttpStatus.OK));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.amazon.dataprepper.plugins;

import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.auth.BasicToken;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.util.UUID;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

class UnauthenticatedArmeriaAuthenticationProviderTest {
@RegisterExtension
static ServerExtension server = new ServerExtension() {
@Override
protected void configure(final ServerBuilder sb) {
sb.service("/test", (ctx, req) -> HttpResponse.of(200));
new UnauthenticatedArmeriaAuthenticationProvider().addAuthenticationDecorator(sb);
}
};

@Test
void httpRequest_without_authentication_responds_OK() {
final WebClient client = WebClient.of(server.httpUri());

final AggregatedHttpResponse httpResponse = client.get("/test").aggregate().join();

assertThat(httpResponse.status(), equalTo(HttpStatus.OK));
}

@Test
void httpRequest_with_BasicAuthentication_responds_OK() {
final WebClient client = WebClient.builder(server.httpUri())
.auth(BasicToken.of(UUID.randomUUID().toString(), UUID.randomUUID().toString()))
.build();

final AggregatedHttpResponse httpResponse = client.get("/test").aggregate().join();

assertThat(httpResponse.status(), equalTo(HttpStatus.OK));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock-maker-inline
Loading