diff --git a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/nessie/IcebergNessieCatalogFactory.java b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/nessie/IcebergNessieCatalogFactory.java index 2b78675540635..b33aa15301279 100644 --- a/presto-iceberg/src/main/java/com/facebook/presto/iceberg/nessie/IcebergNessieCatalogFactory.java +++ b/presto-iceberg/src/main/java/com/facebook/presto/iceberg/nessie/IcebergNessieCatalogFactory.java @@ -59,6 +59,7 @@ protected Map getCatalogProperties(ConnectorSession session) if (hash != null) { properties.put("ref.hash", hash); } + catalogConfig.getAuthenticationType().ifPresent(val -> properties.put("nessie.authentication.type", val.toString())); catalogConfig.getReadTimeoutMillis().ifPresent(val -> properties.put("transport.read-timeout", val.toString())); catalogConfig.getConnectTimeoutMillis().ifPresent(val -> properties.put("transport.connect-timeout", val.toString())); catalogConfig.getClientBuilderImpl().ifPresent(val -> properties.put("client-builder-impl", val)); diff --git a/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSystemTablesNessieWithBearerAuth.java b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSystemTablesNessieWithBearerAuth.java new file mode 100644 index 0000000000000..74c0a0ec78b2b --- /dev/null +++ b/presto-iceberg/src/test/java/com/facebook/presto/iceberg/nessie/TestIcebergSystemTablesNessieWithBearerAuth.java @@ -0,0 +1,101 @@ +/* + * 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.facebook.presto.iceberg.nessie; + +import com.facebook.presto.Session; +import com.facebook.presto.iceberg.IcebergConfig; +import com.facebook.presto.iceberg.IcebergPlugin; +import com.facebook.presto.testing.QueryRunner; +import com.facebook.presto.testing.containers.KeycloakContainer; +import com.facebook.presto.testing.containers.NessieContainer; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.google.common.collect.ImmutableMap; +import org.testcontainers.containers.Network; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; + +import java.nio.file.Path; +import java.util.Map; + +import static com.facebook.presto.iceberg.CatalogType.NESSIE; +import static com.facebook.presto.iceberg.IcebergQueryRunner.ICEBERG_CATALOG; +import static com.facebook.presto.iceberg.IcebergQueryRunner.getIcebergDataDirectoryPath; +import static com.facebook.presto.iceberg.nessie.NessieTestUtil.nessieConnectorProperties; +import static com.facebook.presto.testing.TestingSession.testSessionBuilder; + +public class TestIcebergSystemTablesNessieWithBearerAuth + extends TestIcebergSystemTablesNessie +{ + private NessieContainer nessieContainer; + private KeycloakContainer keycloakContainer; + + @BeforeClass + @Override + public void init() + throws Exception + { + Map envVars = ImmutableMap.builder() + .putAll(NessieContainer.DEFAULT_ENV_VARS) + .put("QUARKUS_OIDC_AUTH_SERVER_URL", KeycloakContainer.SERVER_URL + "/realms/" + KeycloakContainer.MASTER_REALM) + .put("QUARKUS_OIDC_CLIENT_ID", "nessie") + .put("NESSIE_SERVER_AUTHENTICATION_ENABLED", "true") + .buildOrThrow(); + + Network network = Network.newNetwork(); + + nessieContainer = NessieContainer.builder().withEnvVars(envVars).withNetwork(network).build(); + nessieContainer.start(); + keycloakContainer = KeycloakContainer.builder().withNetwork(network).build(); + keycloakContainer.start(); + + super.init(); + } + + @AfterClass(alwaysRun = true) + @Override + public void tearDown() + { + super.tearDown(); + if (nessieContainer != null) { + nessieContainer.stop(); + } + } + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + Session session = testSessionBuilder() + .setCatalog(ICEBERG_CATALOG) + .build(); + + DistributedQueryRunner queryRunner = DistributedQueryRunner.builder(session).build(); + + Path dataDirectory = queryRunner.getCoordinator().getDataDirectory(); + Path catalogDirectory = getIcebergDataDirectoryPath(dataDirectory, "NESSIE", new IcebergConfig().getFileFormat(), false); + + queryRunner.installPlugin(new IcebergPlugin()); + Map icebergProperties = ImmutableMap.builder() + .put("iceberg.catalog.type", String.valueOf(NESSIE)) + .putAll(nessieConnectorProperties(nessieContainer.getRestApiUri())) + .put("iceberg.catalog.warehouse", catalogDirectory.getParent().toFile().toURI().toString()) + .put("iceberg.nessie.auth.type", "BEARER") + .put("iceberg.nessie.auth.bearer.token", keycloakContainer.getAccessToken()) + .build(); + + queryRunner.createCatalog(ICEBERG_CATALOG, "iceberg", icebergProperties); + + return queryRunner; + } +} diff --git a/presto-testing-docker/pom.xml b/presto-testing-docker/pom.xml index 45096037b33d7..a790feb9da5ab 100644 --- a/presto-testing-docker/pom.xml +++ b/presto-testing-docker/pom.xml @@ -13,6 +13,7 @@ presto-testing-docker + 26.0.7 ${project.parent.basedir} true @@ -64,6 +65,24 @@ net.jodah failsafe + + + org.keycloak + keycloak-admin-client + ${dep.keycloak.client.version} + + + org.jboss.resteasy + resteasy-jaxb-provider + + + + + + org.keycloak + keycloak-client-common-synced + ${dep.keycloak.client.version} + diff --git a/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/KeycloakContainer.java b/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/KeycloakContainer.java new file mode 100644 index 0000000000000..8b4d1a40fed52 --- /dev/null +++ b/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/KeycloakContainer.java @@ -0,0 +1,128 @@ +/* + * 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.facebook.presto.testing.containers; + +import com.facebook.airlift.log.Logger; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.representations.idm.RealmRepresentation; +import org.testcontainers.containers.Network; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class KeycloakContainer + extends BaseTestContainer +{ + private static final Logger log = Logger.get(KeycloakContainer.class); + + public static final String DEFAULT_IMAGE = "quay.io/keycloak/keycloak:26.4.2"; + public static final String DEFAULT_HOST_NAME = "keycloak"; + + public static final String DEFAULT_USER_NAME = "admin"; + public static final String DEFAULT_PASSWORD = "admin"; + + public static final String MASTER_REALM = "master"; + public static final String ADMIN_CLI_CLIENT = "admin-cli"; + + public static final int PORT = 8080; + public static final String SERVER_URL = "http://" + DEFAULT_HOST_NAME + ":" + PORT; + + public static Builder builder() + { + return new Builder(); + } + + protected KeycloakContainer(String image, + String hostName, + Set exposePorts, + Map filesToMount, + Map envVars, + Optional network, + int retryLimit) + { + super( + image, + hostName, + exposePorts, + filesToMount, + envVars, + network, + retryLimit); + } + + @Override + protected void setupContainer() + { + super.setupContainer(); + withRunCommand(ImmutableList.of("start-dev")); + } + + @Override + public void start() + { + super.start(); + log.info("Keycloak container started with URL: %s", getUrl()); + } + + public String getUrl() + { + return "http://" + getMappedHostAndPortForExposedPort(PORT); + } + + public String getAccessToken() + { + try (Keycloak keycloak = KeycloakBuilder.builder() + .serverUrl(getUrl()) + .realm(MASTER_REALM) + .clientId(ADMIN_CLI_CLIENT) + .username(DEFAULT_USER_NAME) + .password(DEFAULT_PASSWORD) + .build()) { + RealmResource master = keycloak.realm(MASTER_REALM); + RealmRepresentation masterRep = master.toRepresentation(); + // change access token lifespan from 1 minute (default) to 1 hour + // to keep the token alive in case testcase takes more than a minute to finish execution. + masterRep.setAccessTokenLifespan(3600); + master.update(masterRep); + return keycloak.tokenManager().getAccessTokenString(); + } + } + + public static class Builder + extends BaseTestContainer.Builder + { + private Builder() + { + this.image = DEFAULT_IMAGE; + this.hostName = DEFAULT_HOST_NAME; + this.exposePorts = ImmutableSet.of(PORT); + this.envVars = ImmutableMap.of( + "KC_BOOTSTRAP_ADMIN_USERNAME", DEFAULT_USER_NAME, + "KC_BOOTSTRAP_ADMIN_PASSWORD", DEFAULT_PASSWORD, + "KC_HOSTNAME", SERVER_URL); + } + + @Override + public KeycloakContainer build() + { + return new KeycloakContainer(image, hostName, exposePorts, filesToMount, envVars, network, startupRetryLimit); + } + } +} diff --git a/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/NessieContainer.java b/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/NessieContainer.java index 8042c0afd7fcc..c8f2974f6dc6e 100644 --- a/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/NessieContainer.java +++ b/presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/NessieContainer.java @@ -33,6 +33,10 @@ public class NessieContainer public static final int PORT = 19121; + public static final ImmutableMap DEFAULT_ENV_VARS = ImmutableMap.of( + "QUARKUS_HTTP_PORT", String.valueOf(PORT), + "NESSIE_VERSION_STORE_TYPE", VERSION_STORE_TYPE); + public static Builder builder() { return new Builder(); @@ -63,7 +67,7 @@ private Builder() this.image = DEFAULT_IMAGE; this.hostName = DEFAULT_HOST_NAME; this.exposePorts = ImmutableSet.of(PORT); - this.envVars = ImmutableMap.of("QUARKUS_HTTP_PORT", String.valueOf(PORT), "NESSIE_VERSION_STORE_TYPE", VERSION_STORE_TYPE); + this.envVars = DEFAULT_ENV_VARS; } @Override