Skip to content

Commit f5c6fe6

Browse files
fix: Fix Bearer authentication with Nessie catalog
1 parent 6fc2836 commit f5c6fe6

File tree

5 files changed

+263
-1
lines changed

5 files changed

+263
-1
lines changed

presto-iceberg/src/main/java/com/facebook/presto/iceberg/nessie/IcebergNessieCatalogFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ protected Map<String, String> getCatalogProperties(ConnectorSession session)
5959
if (hash != null) {
6060
properties.put("ref.hash", hash);
6161
}
62+
catalogConfig.getAuthenticationType().ifPresent(val -> properties.put("nessie.authentication.type", val.toString()));
6263
catalogConfig.getReadTimeoutMillis().ifPresent(val -> properties.put("transport.read-timeout", val.toString()));
6364
catalogConfig.getConnectTimeoutMillis().ifPresent(val -> properties.put("transport.connect-timeout", val.toString()));
6465
catalogConfig.getClientBuilderImpl().ifPresent(val -> properties.put("client-builder-impl", val));
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package com.facebook.presto.iceberg.nessie;
15+
16+
import com.facebook.presto.Session;
17+
import com.facebook.presto.iceberg.IcebergConfig;
18+
import com.facebook.presto.iceberg.IcebergPlugin;
19+
import com.facebook.presto.testing.QueryRunner;
20+
import com.facebook.presto.testing.containers.KeycloakContainer;
21+
import com.facebook.presto.testing.containers.NessieContainer;
22+
import com.facebook.presto.tests.DistributedQueryRunner;
23+
import com.google.common.collect.ImmutableMap;
24+
import org.testcontainers.containers.Network;
25+
import org.testng.annotations.AfterClass;
26+
import org.testng.annotations.BeforeClass;
27+
28+
import java.nio.file.Path;
29+
import java.util.Map;
30+
31+
import static com.facebook.presto.iceberg.CatalogType.NESSIE;
32+
import static com.facebook.presto.iceberg.IcebergQueryRunner.ICEBERG_CATALOG;
33+
import static com.facebook.presto.iceberg.IcebergQueryRunner.getIcebergDataDirectoryPath;
34+
import static com.facebook.presto.iceberg.nessie.NessieTestUtil.nessieConnectorProperties;
35+
import static com.facebook.presto.testing.TestingSession.testSessionBuilder;
36+
37+
public class TestIcebergSystemTablesNessieWithBearerAuth
38+
extends TestIcebergSystemTablesNessie
39+
{
40+
private NessieContainer nessieContainer;
41+
private KeycloakContainer keycloakContainer;
42+
43+
@BeforeClass
44+
@Override
45+
public void init()
46+
throws Exception
47+
{
48+
Map<String, String> envVars = ImmutableMap.<String, String>builder()
49+
.putAll(NessieContainer.DEFAULT_ENV_VARS)
50+
.put("QUARKUS_OIDC_AUTH_SERVER_URL", KeycloakContainer.SERVER_URL + "/realms/master")
51+
.put("QUARKUS_OIDC_CLIENT_ID", "projectnessie")
52+
.put("NESSIE_SERVER_AUTHENTICATION_ENABLED", "true")
53+
.buildOrThrow();
54+
55+
Network network = Network.newNetwork();
56+
57+
nessieContainer = NessieContainer.builder().withEnvVars(envVars).withNetwork(network).build();
58+
nessieContainer.start();
59+
keycloakContainer = KeycloakContainer.builder().withNetwork(network).build();
60+
keycloakContainer.start();
61+
super.init();
62+
}
63+
64+
@AfterClass(alwaysRun = true)
65+
@Override
66+
public void tearDown()
67+
{
68+
super.tearDown();
69+
if (nessieContainer != null) {
70+
nessieContainer.stop();
71+
}
72+
}
73+
74+
@Override
75+
protected QueryRunner createQueryRunner()
76+
throws Exception
77+
{
78+
Session session = testSessionBuilder()
79+
.setCatalog(ICEBERG_CATALOG)
80+
.build();
81+
82+
DistributedQueryRunner queryRunner = DistributedQueryRunner.builder(session).build();
83+
84+
Path dataDirectory = queryRunner.getCoordinator().getDataDirectory();
85+
Path catalogDirectory = getIcebergDataDirectoryPath(dataDirectory, "NESSIE", new IcebergConfig().getFileFormat(), false);
86+
87+
queryRunner.installPlugin(new IcebergPlugin());
88+
Map<String, String> icebergProperties = ImmutableMap.<String, String>builder()
89+
.put("iceberg.catalog.type", String.valueOf(NESSIE))
90+
.putAll(nessieConnectorProperties(nessieContainer.getRestApiUri()))
91+
.put("iceberg.catalog.warehouse", catalogDirectory.getParent().toFile().toURI().toString())
92+
.put("iceberg.nessie.auth.type", "BEARER")
93+
.put("iceberg.nessie.auth.bearer.token", keycloakContainer.getAccessToken())
94+
.build();
95+
96+
queryRunner.createCatalog(ICEBERG_CATALOG, "iceberg", icebergProperties);
97+
98+
return queryRunner;
99+
}
100+
}

presto-testing-docker/pom.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
<description>presto-testing-docker</description>
1414

1515
<properties>
16+
<dep.keycloak.client.version>21.1.2</dep.keycloak.client.version>
17+
<dep.keycloak.version>26.4.2</dep.keycloak.version>
1618
<air.main.basedir>${project.parent.basedir}</air.main.basedir>
1719
<air.check.skip-modernizer>true</air.check.skip-modernizer>
1820
</properties>
@@ -64,6 +66,33 @@
6466
<groupId>net.jodah</groupId>
6567
<artifactId>failsafe</artifactId>
6668
</dependency>
69+
70+
71+
<dependency>
72+
<groupId>org.keycloak</groupId>
73+
<artifactId>keycloak-admin-client-jakarta</artifactId>
74+
<version>${dep.keycloak.client.version}</version>
75+
<exclusions>
76+
<exclusion>
77+
<groupId>org.jboss.resteasy</groupId>
78+
<artifactId>resteasy-jaxb-provider</artifactId>
79+
</exclusion>
80+
<exclusion>
81+
<groupId>org.jboss.spec.javax.ws.rs</groupId>
82+
<artifactId>jboss-jaxrs-api_3.0_spec</artifactId>
83+
</exclusion>
84+
<exclusion>
85+
<groupId>org.keycloak</groupId>
86+
<artifactId>keycloak-common</artifactId>
87+
</exclusion>
88+
</exclusions>
89+
</dependency>
90+
91+
<dependency>
92+
<groupId>org.keycloak</groupId>
93+
<artifactId>keycloak-core</artifactId>
94+
<version>${dep.keycloak.version}</version>
95+
</dependency>
6796
</dependencies>
6897

6998
<build>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package com.facebook.presto.testing.containers;
15+
16+
import com.facebook.airlift.log.Logger;
17+
import com.google.common.collect.ImmutableList;
18+
import com.google.common.collect.ImmutableMap;
19+
import com.google.common.collect.ImmutableSet;
20+
import org.keycloak.admin.client.Keycloak;
21+
import org.keycloak.admin.client.KeycloakBuilder;
22+
import org.keycloak.admin.client.resource.RealmResource;
23+
import org.keycloak.representations.idm.RealmRepresentation;
24+
import org.testcontainers.containers.Network;
25+
26+
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.Set;
29+
30+
public class KeycloakContainer
31+
extends BaseTestContainer
32+
{
33+
private static final Logger log = Logger.get(KeycloakContainer.class);
34+
35+
public static final String DEFAULT_IMAGE = "quay.io/keycloak/keycloak:26.4.2";
36+
public static final String DEFAULT_HOST_NAME = "keycloak";
37+
38+
public static final String DEFAULT_USER_NAME = "admin";
39+
public static final String DEFAULT_PASSWORD = "admin";
40+
41+
public static final int PORT = 8080;
42+
public static final String SERVER_URL = "http://" + DEFAULT_HOST_NAME + ":" + PORT;
43+
44+
public static Builder builder()
45+
{
46+
return new Builder();
47+
}
48+
49+
protected KeycloakContainer(String image,
50+
String hostName,
51+
Set<Integer> exposePorts,
52+
Map<String, String> filesToMount,
53+
Map<String, String> envVars,
54+
Optional<Network> network,
55+
int retryLimit)
56+
{
57+
super(
58+
image,
59+
hostName,
60+
exposePorts,
61+
filesToMount,
62+
envVars,
63+
network,
64+
retryLimit);
65+
}
66+
67+
@Override
68+
protected void setupContainer()
69+
{
70+
super.setupContainer();
71+
withRunCommand(ImmutableList.of("start-dev"));
72+
}
73+
74+
@Override
75+
public void start()
76+
{
77+
super.start();
78+
log.info("Keycloak container started with URL: %s", getUrl());
79+
}
80+
81+
public String getUrl()
82+
{
83+
return "http://" + getMappedHostAndPortForExposedPort(PORT);
84+
}
85+
86+
public String getAccessToken()
87+
{
88+
String realm = "master";
89+
String clientId = "admin-cli";
90+
91+
try (Keycloak keycloak = KeycloakBuilder.builder()
92+
.serverUrl(getUrl())
93+
.realm(realm)
94+
.clientId(clientId)
95+
.username(DEFAULT_USER_NAME)
96+
.password(DEFAULT_PASSWORD)
97+
.build()) {
98+
RealmResource master = keycloak.realms().realm(realm);
99+
RealmRepresentation masterRep = master.toRepresentation();
100+
// change access token lifespan from 1 minute (default) to 1 hour
101+
// to keep the token alive in case testcase takes more than a minute to finish execution.
102+
masterRep.setAccessTokenLifespan(3600);
103+
master.update(masterRep);
104+
return keycloak.tokenManager().grantToken().getToken();
105+
}
106+
}
107+
108+
public static class Builder
109+
extends BaseTestContainer.Builder<KeycloakContainer.Builder, KeycloakContainer>
110+
{
111+
private Builder()
112+
{
113+
this.image = DEFAULT_IMAGE;
114+
this.hostName = DEFAULT_HOST_NAME;
115+
this.exposePorts = ImmutableSet.of(PORT);
116+
this.envVars = ImmutableMap.of(
117+
"KEYCLOAK_ADMIN", DEFAULT_USER_NAME,
118+
"KEYCLOAK_ADMIN_PASSWORD", DEFAULT_PASSWORD,
119+
"KC_HOSTNAME_URL", SERVER_URL);
120+
}
121+
122+
@Override
123+
public KeycloakContainer build()
124+
{
125+
return new KeycloakContainer(image, hostName, exposePorts, filesToMount, envVars, network, startupRetryLimit);
126+
}
127+
}
128+
}

presto-testing-docker/src/main/java/com/facebook/presto/testing/containers/NessieContainer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public class NessieContainer
3333

3434
public static final int PORT = 19121;
3535

36+
public static final ImmutableMap<String, String> DEFAULT_ENV_VARS = ImmutableMap.of(
37+
"QUARKUS_HTTP_PORT", String.valueOf(PORT),
38+
"NESSIE_VERSION_STORE_TYPE", VERSION_STORE_TYPE);
39+
3640
public static Builder builder()
3741
{
3842
return new Builder();
@@ -63,7 +67,7 @@ private Builder()
6367
this.image = DEFAULT_IMAGE;
6468
this.hostName = DEFAULT_HOST_NAME;
6569
this.exposePorts = ImmutableSet.of(PORT);
66-
this.envVars = ImmutableMap.of("QUARKUS_HTTP_PORT", String.valueOf(PORT), "NESSIE_VERSION_STORE_TYPE", VERSION_STORE_TYPE);
70+
this.envVars = DEFAULT_ENV_VARS;
6771
}
6872

6973
@Override

0 commit comments

Comments
 (0)