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

Add http credentials provider #136

Merged
merged 1 commit into from
Aug 7, 2024
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
6 changes: 6 additions & 0 deletions trino-aws-proxy/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>trino-aws-proxy-spark3</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.airlift.log.Logger;
import io.trino.aws.proxy.server.credentials.CredentialsController;
import io.trino.aws.proxy.server.credentials.file.FileBasedCredentialsModule;
import io.trino.aws.proxy.server.credentials.http.HttpCredentialsModule;
import io.trino.aws.proxy.server.remote.RemoteS3Module;
import io.trino.aws.proxy.server.rest.RequestFilter;
import io.trino.aws.proxy.server.rest.RequestLoggerController;
Expand Down Expand Up @@ -119,6 +120,7 @@ protected void setup(Binder binder)
// provided implementations
install(new FileBasedCredentialsModule());
install(new OpaS3SecurityModule());
install(new HttpCredentialsModule());

// AssumedRoleProvider binder
configBinder(binder).bindConfig(AssumedRoleProviderConfig.class);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 io.trino.aws.proxy.server.credentials.http;

import com.google.inject.BindingAnnotation;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@BindingAnnotation
@Target({FIELD, PARAMETER, METHOD})
@Retention(RUNTIME)
public @interface ForHttpCredentialsProvider {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 io.trino.aws.proxy.server.credentials.http;

import com.google.inject.Binder;
import io.airlift.configuration.AbstractConfigurationAwareModule;

import static io.airlift.configuration.ConfigBinder.configBinder;
import static io.airlift.http.client.HttpClientBinder.httpClientBinder;
import static io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerBinding.credentialsProviderModule;

public class HttpCredentialsModule
extends AbstractConfigurationAwareModule
{
// set as config value for "credentials-provider.type"
public static final String HTTP_CREDENTIALS_PROVIDER_IDENTIFIER = "http";
public static final String HTTP_CREDENTIALS_PROVIDER_HTTP_CLIENT_NAME = "http-credentials-provider";

@Override
protected void setup(Binder binder)
{
install(credentialsProviderModule(
HTTP_CREDENTIALS_PROVIDER_IDENTIFIER,
HttpCredentialsProvider.class,
innerBinder -> {
configBinder(innerBinder).bindConfig(HttpCredentialsProviderConfig.class);
innerBinder.bind(HttpCredentialsProvider.class);
httpClientBinder(innerBinder).bindHttpClient(HTTP_CREDENTIALS_PROVIDER_HTTP_CLIENT_NAME, ForHttpCredentialsProvider.class);
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 io.trino.aws.proxy.server.credentials.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.inject.Inject;
import io.airlift.http.client.FullJsonResponseHandler.JsonResponse;
import io.airlift.http.client.HttpClient;
import io.airlift.http.client.HttpStatus;
import io.airlift.http.client.Request;
import io.airlift.json.JsonCodec;
import io.airlift.json.JsonCodecFactory;
import io.trino.aws.proxy.spi.credentials.Credentials;
import io.trino.aws.proxy.spi.credentials.CredentialsProvider;
import io.trino.aws.proxy.spi.credentials.Identity;
import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;
import java.util.Optional;

import static io.airlift.http.client.FullJsonResponseHandler.createFullJsonResponseHandler;
import static io.airlift.http.client.Request.Builder.prepareGet;
import static java.util.Objects.requireNonNull;

public class HttpCredentialsProvider
pranavr12 marked this conversation as resolved.
Show resolved Hide resolved
implements CredentialsProvider
{
private final HttpClient httpClient;
private final JsonCodec<Credentials> jsonCodec;
private final URI httpCredentialsProviderEndpoint;

@Inject
public HttpCredentialsProvider(@ForHttpCredentialsProvider HttpClient httpClient, HttpCredentialsProviderConfig config, ObjectMapper objectMapper, Class<? extends Identity> identityClass)
{
requireNonNull(objectMapper, "objectMapper is null");
this.httpClient = requireNonNull(httpClient, "httpClient is null");
this.httpCredentialsProviderEndpoint = config.getEndpoint();
ObjectMapper adjustedObjectMapper = objectMapper.registerModule(new SimpleModule().addAbstractTypeMapping(Identity.class, identityClass));
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we are mapping abstract types, it is not possible to have the jsonCodecBinder registered in the module. However, once the abstract type has been mapped at a higher level, we could register the jsonCodecBinder. I'll address that in a separate PR since this change affects the FileBasedCredentialsProvider as well.

Copy link
Member

Choose a reason for hiding this comment

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

this.jsonCodec = new JsonCodecFactory(() -> adjustedObjectMapper).jsonCodec(Credentials.class);
}

@Override
public Optional<Credentials> credentials(String emulatedAccessKey, Optional<String> session)
{
UriBuilder uriBuilder = UriBuilder.fromUri(httpCredentialsProviderEndpoint).path(emulatedAccessKey);
session.ifPresent(sessionToken -> uriBuilder.queryParam("sessionToken", sessionToken));
Request request = prepareGet()
.setUri(uriBuilder.build())
.build();
JsonResponse<Credentials> response = httpClient.execute(request, createFullJsonResponseHandler(jsonCodec));
Copy link
Member

Choose a reason for hiding this comment

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

If the endpoint returns a 404 I imagine it would not return a properly formatted JSON that we could decode.

Should we wrap this in a try / catch and return Optional.empty() if we get status 404 or otherwise propagate the exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here are the possible scenarios:
Server returns 404 -> Credentials provider returns Optional.empty()
Server returns 200, but incorrect response -> The parsing fails and the response value is null (No exception is thrown). I've added a condition to check the null status. Credentials provider returns Optional.empty()
Server return 200, with correct response: Parsing succeeds and the correct value is returned

if (response.getStatusCode() == HttpStatus.NOT_FOUND.code() || !response.hasValue()) {
return Optional.empty();
}
return Optional.of(response.getValue());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 io.trino.aws.proxy.server.credentials.http;

import io.airlift.configuration.Config;
import jakarta.validation.constraints.NotNull;

import java.net.URI;

public class HttpCredentialsProviderConfig
{
private URI endpoint;

@NotNull
public URI getEndpoint()
{
return endpoint;
}

@Config("credentials-provider.http.endpoint")
public HttpCredentialsProviderConfig setEndpoint(String endpoint)
{
this.endpoint = URI.create(endpoint);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* 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 io.trino.aws.proxy.server.credentials.http;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import io.airlift.http.server.HttpServerConfig;
import io.airlift.http.server.HttpServerInfo;
import io.airlift.http.server.testing.TestingHttpServer;
import io.airlift.json.ObjectMapperProvider;
import io.airlift.node.NodeInfo;
import io.trino.aws.proxy.server.testing.TestingIdentity;
import io.trino.aws.proxy.server.testing.TestingTrinoAwsProxyServer;
import io.trino.aws.proxy.server.testing.harness.BuilderFilter;
import io.trino.aws.proxy.server.testing.harness.TrinoAwsProxyTest;
import io.trino.aws.proxy.spi.credentials.Credential;
import io.trino.aws.proxy.spi.credentials.Credentials;
import io.trino.aws.proxy.spi.credentials.CredentialsProvider;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Optional;

import static io.trino.aws.proxy.server.credentials.http.HttpCredentialsModule.HTTP_CREDENTIALS_PROVIDER_IDENTIFIER;
import static io.trino.aws.proxy.spi.plugin.TrinoAwsProxyServerBinding.bindIdentityType;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static java.util.Objects.requireNonNull;
import static org.assertj.core.api.Assertions.assertThat;

@TrinoAwsProxyTest(filters = TestHttpCredentialsProvider.Filter.class)
public class TestHttpCredentialsProvider
{
private final CredentialsProvider credentialsProvider;

public static class Filter
implements BuilderFilter
{
@Override
public TestingTrinoAwsProxyServer.Builder filter(TestingTrinoAwsProxyServer.Builder builder)
{
TestingHttpServer httpCredentialsServer;
try {
httpCredentialsServer = createTestingHttpCredentialsServer();
httpCredentialsServer.start();
}
catch (Exception e) {
throw new RuntimeException("Failed to start test http credentials provider server", e);
}
return builder.withoutTestingCredentialsRoleProviders()
.addModule(new HttpCredentialsModule())
.addModule(binder -> bindIdentityType(binder, TestingIdentity.class))
.withProperty("credentials-provider.type", HTTP_CREDENTIALS_PROVIDER_IDENTIFIER)
.withProperty("credentials-provider.http.endpoint", httpCredentialsServer.getBaseUrl().toString());
}
}

@Inject
public TestHttpCredentialsProvider(CredentialsProvider credentialsProvider)
{
this.credentialsProvider = requireNonNull(credentialsProvider, "credentialsProvider is null");
}

@Test
public void testValidCredentialsWithEmptySession()
{
Credential emulated = new Credential("test-emulated-access-key", "test-emulated-secret");
Credential remote = new Credential("test-remote-access-key", "test-remote-secret");
Credentials expected = new Credentials(emulated, Optional.of(remote), Optional.empty(), Optional.of(new TestingIdentity("test-username", ImmutableList.of(), "xyzpdq")));
Optional<Credentials> actual = credentialsProvider.credentials("test-emulated-access-key", Optional.empty());
assertThat(actual).contains(expected);
}

@Test
public void testValidCredentialsWithValidSession()
{
Credential emulated = new Credential("test-emulated-access-key", "test-emulated-secret");
Credential remote = new Credential("test-remote-access-key", "test-remote-secret");
Credentials expected = new Credentials(emulated, Optional.of(remote), Optional.empty(), Optional.of(new TestingIdentity("test-username", ImmutableList.of(), "xyzpdq")));
Optional<Credentials> actual = credentialsProvider.credentials("test-emulated-access-key", Optional.of("test-emulated-access-key"));
assertThat(actual).contains(expected);
}

@Test
public void testInvalidCredentialsWithEmptySession()
{
Optional<Credentials> actual = credentialsProvider.credentials("non-existent-key", Optional.empty());
assertThat(actual).isEmpty();
}

@Test
public void testValidCredentialsWithInvalidSession()
{
Optional<Credentials> actual = credentialsProvider.credentials("test-emulated-access-key", Optional.of("sessionToken-not-equals-accessKey"));
assertThat(actual).isEmpty();
}

@Test
public void testInvalidCredentialsWithInvalidSession()
{
Optional<Credentials> actual = credentialsProvider.credentials("non-existent-key", Optional.of("sessionToken-not-equals-accessKey"));
assertThat(actual).isEmpty();
}

@Test
public void testIncorrectResponseFromServer()
{
Optional<Credentials> actual = credentialsProvider.credentials("incorrect-response", Optional.empty());
assertThat(actual).isEmpty();
}

private static TestingHttpServer createTestingHttpCredentialsServer()
throws IOException
{
NodeInfo nodeInfo = new NodeInfo("test");
HttpServerConfig config = new HttpServerConfig().setHttpPort(0);
HttpServerInfo httpServerInfo = new HttpServerInfo(config, nodeInfo);
return new TestingHttpServer(httpServerInfo, nodeInfo, config, new HttpCredentialsServlet(), ImmutableMap.of());
}

private static class HttpCredentialsServlet
extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException
{
Optional<String> sessionToken = Optional.ofNullable(request.getParameter("sessionToken"));
String emulatedAccessKey = request.getPathInfo().substring(1);
String credentialsIdentifier = "";
if (sessionToken.isPresent()) {
// Simulate valid session - When accessKey equals sessionToken
if (emulatedAccessKey.equals(sessionToken.get())) {
credentialsIdentifier = sessionToken.get();
}
}
else {
credentialsIdentifier = emulatedAccessKey;
}
switch (credentialsIdentifier) {
case "test-emulated-access-key" -> {
Credential emulated = new Credential("test-emulated-access-key", "test-emulated-secret");
Credential remote = new Credential("test-remote-access-key", "test-remote-secret");
Credentials credentials = new Credentials(emulated, Optional.of(remote), Optional.empty(), Optional.of(new TestingIdentity("test-username", ImmutableList.of(), "xyzpdq")));
String jsonCredentials = new ObjectMapperProvider().get().writeValueAsString(credentials);
response.setContentType(APPLICATION_JSON);
response.getWriter().print(jsonCredentials);
}
case "incorrect-response" -> {
response.getWriter().print("incorrect response");
}
default -> response.setStatus(HttpServletResponse.SC_NOT_FOUND);
}
}
}
}
Loading
Loading