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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.iceberg.common.DynMethods;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.base.Strings;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.util.PropertyUtil;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
Expand Down Expand Up @@ -118,8 +119,11 @@ public AwsClientProperties(Map<String, String> properties) {
Predicate.not(property -> property.startsWith(CLIENT_CREDENTIAL_PROVIDER_PREFIX))),
PropertyUtil.propertiesWithPrefix(properties, CLIENT_CREDENTIAL_PROVIDER_PREFIX));
this.refreshCredentialsEndpoint =
RESTUtil.resolveEndpoint(
properties.get(CatalogProperties.URI), properties.get(REFRESH_CREDENTIALS_ENDPOINT));
properties.containsKey(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
? properties.get(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
: RESTUtil.resolveEndpoint(
properties.get(CatalogProperties.URI),
properties.get(REFRESH_CREDENTIALS_ENDPOINT));
this.refreshCredentialsEnabled =
PropertyUtil.propertyAsBoolean(properties, REFRESH_CREDENTIALS_ENABLED, true);
this.legacyMd5pluginEnabled =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.iceberg.CatalogProperties;
import org.apache.iceberg.aws.AwsClientProperties;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.base.Strings;
import org.apache.iceberg.rest.ErrorHandlers;
import org.apache.iceberg.rest.HTTPClient;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.apache.iceberg.rest.RESTClient;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.auth.AuthManager;
Expand Down Expand Up @@ -56,7 +58,14 @@ public class VendedCredentialsProvider implements AwsCredentialsProvider, SdkAut

private VendedCredentialsProvider(Map<String, String> properties) {
Preconditions.checkArgument(null != properties, "Invalid properties: null");
Preconditions.checkArgument(null != properties.get(URI), "Invalid credentials endpoint: null");
this.credentialsEndpoint =
properties.containsKey(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
? properties.get(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
: RESTUtil.resolveEndpoint(
properties.get(CatalogProperties.URI),
properties.getOrDefault(
URI, properties.get(AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT)));
Preconditions.checkArgument(null != credentialsEndpoint, "Invalid credentials endpoint: null");
Preconditions.checkArgument(
null != properties.get(CatalogProperties.URI), "Invalid catalog endpoint: null");
this.properties = properties;
Expand All @@ -65,7 +74,6 @@ private VendedCredentialsProvider(Map<String, String> properties) {
.cachedValueName(VendedCredentialsProvider.class.getName())
.build();
this.catalogEndpoint = properties.get(CatalogProperties.URI);
this.credentialsEndpoint = properties.get(URI);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.apache.iceberg.aws.s3.VendedCredentialsProvider;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.apache.iceberg.rest.auth.OAuth2Properties;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
Expand Down Expand Up @@ -143,6 +144,22 @@ public void refreshCredentialsEndpoint() {
.isInstanceOf(VendedCredentialsProvider.class);
}

@Test
void usesCatalogCredentialsEndpoint() {
String endpoint = "https://catalog.example/table/credentials";
AwsClientProperties properties =
new AwsClientProperties(
ImmutableMap.of(
CatalogProperties.URI, "https://catalog.example",
RESTCatalogProperties.CREDENTIALS_ENDPOINT, endpoint,
AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT, "/legacy"));

try (VendedCredentialsProvider provider =
(VendedCredentialsProvider) properties.credentialsProvider("key", "secret", "token")) {
assertThat(provider).extracting("credentialsEndpoint").isEqualTo(endpoint);
}
}

@Test
public void refreshCredentialsEndpointSetButRefreshDisabled() {
AwsClientProperties awsClientProperties =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.apache.iceberg.io.StorageCredential;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.rest.HttpMethod;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.apache.iceberg.rest.credentials.ImmutableCredential;
import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
Expand All @@ -43,6 +44,8 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;
Expand Down Expand Up @@ -71,8 +74,9 @@ public void before() {
mockServer.reset();
}

@Test
public void credentialRefreshSchedulesNextRefresh() {
@ParameterizedTest
@ValueSource(strings = {"rest.credentials.endpoint", "client.refresh-credentials-endpoint"})
public void credentialRefreshSchedulesNextRefresh(String endpointProperty) {
String nearExpiryMs = Long.toString(Instant.now().plus(3, ChronoUnit.MINUTES).toEpochMilli());

StorageCredential initialCredential =
Expand Down Expand Up @@ -145,8 +149,10 @@ public void credentialRefreshSchedulesNextRefresh() {
ImmutableMap.of(
AwsProperties.CLIENT_FACTORY,
StaticClientFactory.class.getName(),
VendedCredentialsProvider.URI,
CREDENTIALS_URI,
endpointProperty,
endpointProperty.equals(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
? CREDENTIALS_URI
: "/credentials",
CatalogProperties.URI,
CATALOG_URI,
"init-creation-stacktrace",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,54 @@ public void noS3Credentials() {
}
}

@Test
public void resolvesRelativeCredentialsUri() {
HttpRequest mockRequest = request("/v1/credentials").withMethod(HttpMethod.GET.name());
HttpResponse mockResponse =
response(
LoadCredentialsResponseParser.toJson(
ImmutableLoadCredentialsResponse.builder().build()))
.withStatusCode(200);
mockServer.when(mockRequest).respond(mockResponse);

try (VendedCredentialsProvider provider =
VendedCredentialsProvider.create(
ImmutableMap.of(
VendedCredentialsProvider.URI,
"/credentials",
CatalogProperties.URI,
CATALOG_URI))) {
assertThat(provider.fetchCredentials().credentials()).isEmpty();
}

mockServer.verify(mockRequest, VerificationTimes.once());
}

@Test
public void usesCatalogCredentialsEndpoint() {
HttpRequest mockRequest = request("/v1/credentials").withMethod(HttpMethod.GET.name());
HttpResponse mockResponse =
response(
LoadCredentialsResponseParser.toJson(
ImmutableLoadCredentialsResponse.builder().build()))
.withStatusCode(200);
mockServer.when(mockRequest).respond(mockResponse);

try (VendedCredentialsProvider provider =
VendedCredentialsProvider.create(
ImmutableMap.of(
RESTCatalogProperties.CREDENTIALS_ENDPOINT,
CREDENTIALS_URI,
VendedCredentialsProvider.URI,
"https://legacy.example/wrong",
CatalogProperties.URI,
CATALOG_URI))) {
assertThat(provider.fetchCredentials().credentials()).isEmpty();
}

mockServer.verify(mockRequest, VerificationTimes.once());
}

@Test
public void accessKeyIdAndSecretAccessKeyWithoutToken() {
HttpRequest mockRequest = request("/v1/credentials").withMethod(HttpMethod.GET.name());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ public static ScanPlanningMode fromString(String mode) {
*/
public static final String REMOTE_SIGNING_ENDPOINT = "rest.remote-signing.endpoint";

/**
* The absolute endpoint for refreshing table storage credentials.
*
* <p>The REST catalog client sets this property when creating a table FileIO with vended
* credentials. FileIO implementations use it in preference to their legacy refresh properties.
*/
public static final String CREDENTIALS_ENDPOINT = "rest.credentials.endpoint";

/**
* The {@link RemoteSigningConfig}, JSON-encoded.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,17 @@ private FileIO tableFileIO(
RemoteSigningConfigParser.toJson(remoteSigningConfig));
}

return newFileIO(context, fullConf.buildKeepingLast(), storageCredentials);
Map<String, String> ioProperties = Maps.newHashMap(fullConf.buildKeepingLast());
ioProperties.remove(RESTCatalogProperties.CREDENTIALS_ENDPOINT);
if (!storageCredentials.isEmpty()) {
ioProperties.put(
RESTCatalogProperties.CREDENTIALS_ENDPOINT,
RESTUtil.resolveEndpoint(
properties().get(CatalogProperties.URI),
paths.table(tableIdentifier) + "/credentials"));
}

return newFileIO(context, ioProperties, storageCredentials);
}

/**
Expand Down
75 changes: 75 additions & 0 deletions core/src/test/java/org/apache/iceberg/rest/TestRESTCatalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -3881,6 +3881,81 @@ public void testSequenceNumberConflictThrowsCommitFailed() {
.hasMessageContaining("Validation failed, please retry");
}

@ParameterizedTest
@ValueSource(booleans = {true, false})
void configuresCredentialsEndpointOnlyWithStorageCredentials(boolean hasCredentials)
throws IOException {
Credential credential =
ImmutableCredential.builder()
.prefix("s3://test-bucket/")
.putConfig("s3.access-key-id", "test-access-key")
.putConfig("s3.secret-access-key", "test-secret-key")
.build();

RESTCatalogAdapter adapter =
new RESTCatalogAdapter(backendCatalog) {
@SuppressWarnings("unchecked")
@Override
public <T extends RESTResponse> T handleRequest(
Route route,
Map<String, String> vars,
HTTPRequest httpRequest,
Class<T> responseType,
Consumer<Map<String, String>> responseHeaders) {
T response =
super.handleRequest(route, vars, httpRequest, responseType, responseHeaders);
if (route == Route.LOAD_TABLE && response instanceof LoadTableResponse loadResponse) {
LoadTableResponse.Builder builder =
LoadTableResponse.builder()
.withTableMetadata(loadResponse.tableMetadata())
.addAllConfig(loadResponse.config())
.addConfig("client.refresh-credentials-endpoint", "v1/legacy/credentials")
.addConfig(RESTCatalogProperties.CREDENTIALS_ENDPOINT, "unresolved");
if (hasCredentials) {
builder.addCredential(credential);
} else {
builder.addConfig("s3.remote-signing-enabled", "true");
}
return (T) builder.build();
}
return response;
}
};

AtomicReference<Map<String, String>> fileIOProperties = new AtomicReference<>();
try (RESTCatalog catalog =
new RESTCatalog(SessionCatalog.SessionContext.createEmpty(), config -> adapter) {
@Override
protected RESTSessionCatalog newSessionCatalog(
Function<Map<String, String>, RESTClient> clientBuilder) {
return new RESTSessionCatalog(
clientBuilder,
(context, config) -> {
fileIOProperties.set(config);
return new TestCatalogUtil.TestFileIOWithStorageCredentials();
});
}
}) {
String catalogUri = "https://catalog.example/api/catalog/";
catalog.initialize("test", ImmutableMap.of(CatalogProperties.URI, catalogUri));
assertThat(fileIOProperties.get())
.doesNotContainKey(RESTCatalogProperties.CREDENTIALS_ENDPOINT);
catalog.createNamespace(NS);
catalog.createTable(TABLE, SCHEMA);
catalog.loadTable(TABLE);

if (hasCredentials) {
assertThat(fileIOProperties.get())
.containsEntry(
RESTCatalogProperties.CREDENTIALS_ENDPOINT,
RESTUtil.resolveEndpoint(catalogUri, RESOURCE_PATHS.table(TABLE) + "/credentials"));
} else {
assertThat(fileIOProperties.get())
.doesNotContainKey(RESTCatalogProperties.CREDENTIALS_ENDPOINT);
}
}
}

@Test
public void testIoBuilderReceivesStorageCredentials() {
Credential credential =
Expand Down
9 changes: 6 additions & 3 deletions gcp/src/main/java/org/apache/iceberg/gcp/GCPProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.util.PropertyUtil;

Expand Down Expand Up @@ -168,9 +169,11 @@ public GCPProperties(Map<String, String> properties) {
}

gcsOauth2RefreshCredentialsEndpoint =
RESTUtil.resolveEndpoint(
properties.get(CatalogProperties.URI),
properties.get(GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT));
properties.containsKey(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
? properties.get(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
: RESTUtil.resolveEndpoint(
properties.get(CatalogProperties.URI),
properties.get(GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT));
gcsOauth2RefreshCredentialsEnabled =
PropertyUtil.propertyAsBoolean(properties, GCS_OAUTH2_REFRESH_CREDENTIALS_ENABLED, true);
gcsNoAuth = Boolean.parseBoolean(properties.getOrDefault(GCS_NO_AUTH, "false"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.rest.ErrorHandlers;
import org.apache.iceberg.rest.HTTPClient;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.apache.iceberg.rest.RESTClient;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.auth.AuthManager;
Expand All @@ -51,13 +52,15 @@ public class OAuth2RefreshCredentialsHandler
private AuthSession authSession;

private OAuth2RefreshCredentialsHandler(Map<String, String> properties) {
Preconditions.checkArgument(
null != properties.get(GCPProperties.GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT),
"Invalid credentials endpoint: null");
this.credentialsEndpoint =
properties.containsKey(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
? properties.get(RESTCatalogProperties.CREDENTIALS_ENDPOINT)
: RESTUtil.resolveEndpoint(
properties.get(CatalogProperties.URI),
properties.get(GCPProperties.GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT));
Preconditions.checkArgument(null != credentialsEndpoint, "Invalid credentials endpoint: null");
Preconditions.checkArgument(
null != properties.get(CatalogProperties.URI), "Invalid catalog endpoint: null");
this.credentialsEndpoint =
properties.get(GCPProperties.GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT);
this.catalogEndpoint = properties.get(CatalogProperties.URI);
this.properties = properties;
}
Expand Down
15 changes: 15 additions & 0 deletions gcp/src/test/java/org/apache/iceberg/gcp/TestGCPProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;

import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.rest.RESTCatalogProperties;
import org.junit.jupiter.api.Test;

public class TestGCPProperties {
Expand Down Expand Up @@ -62,6 +63,20 @@ public void refreshCredentialsEndpointSet() {
.isEqualTo("/v1/credentials");
}

@Test
void usesCatalogCredentialsEndpoint() {
String endpoint = "https://catalog.example/table/credentials";
GCPProperties properties =
new GCPProperties(
ImmutableMap.of(
RESTCatalogProperties.CREDENTIALS_ENDPOINT,
endpoint,
GCS_OAUTH2_REFRESH_CREDENTIALS_ENDPOINT,
"/legacy"));

assertThat(properties.oauth2RefreshCredentialsEndpoint()).contains(endpoint);
}

@Test
public void refreshCredentialsEndpointSetButRefreshDisabled() {
GCPProperties gcpProperties =
Expand Down
Loading
Loading