diff --git a/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java b/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java index 580b801149a1..219688b74ee5 100644 --- a/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java +++ b/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java @@ -120,7 +120,8 @@ public QueryRunner( kerberosConfigPath.map(File::new), kerberosKeytabPath.map(File::new), kerberosCredentialCachePath.map(File::new), - delegatedKerberos); + delegatedKerberos, + Optional.empty()); } this.httpClient = builder.build(); diff --git a/client/trino-client/src/main/java/io/trino/client/OkHttpUtil.java b/client/trino-client/src/main/java/io/trino/client/OkHttpUtil.java index 29d5e68b7754..7d9a2935f611 100644 --- a/client/trino-client/src/main/java/io/trino/client/OkHttpUtil.java +++ b/client/trino-client/src/main/java/io/trino/client/OkHttpUtil.java @@ -16,6 +16,7 @@ import com.google.common.base.CharMatcher; import com.google.common.base.StandardSystemProperty; import com.google.common.net.HostAndPort; +import io.trino.client.auth.kerberos.DelegatedConstrainedContextProvider; import io.trino.client.auth.kerberos.DelegatedUnconstrainedContextProvider; import io.trino.client.auth.kerberos.GSSContextProvider; import io.trino.client.auth.kerberos.LoginBasedUnconstrainedContextProvider; @@ -25,6 +26,7 @@ import okhttp3.JavaNetCookieJar; import okhttp3.OkHttpClient; import okhttp3.internal.tls.LegacyHostnameVerifier; +import org.ietf.jgss.GSSCredential; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -315,11 +317,12 @@ public static void setupKerberos( Optional kerberosConfig, Optional keytab, Optional credentialCache, - boolean delegatedKerberos) + boolean delegatedKerberos, + Optional gssCredential) { GSSContextProvider contextProvider; if (delegatedKerberos) { - contextProvider = new DelegatedUnconstrainedContextProvider(); + contextProvider = getDelegatedGSSContextProvider(gssCredential); } else { contextProvider = new LoginBasedUnconstrainedContextProvider(principal, kerberosConfig, keytab, credentialCache); @@ -333,4 +336,11 @@ public static void setupAlternateHostnameVerification(OkHttpClient.Builder clien { clientBuilder.hostnameVerifier((hostname, session) -> LegacyHostnameVerifier.INSTANCE.verify(alternativeHostname, session)); } + + private static GSSContextProvider getDelegatedGSSContextProvider(Optional gssCredential) + { + return gssCredential.map(DelegatedConstrainedContextProvider::new) + .map(gssCred -> (GSSContextProvider) gssCred) + .orElse(new DelegatedUnconstrainedContextProvider()); + } } diff --git a/client/trino-client/src/main/java/io/trino/client/auth/kerberos/DelegatedConstrainedContextProvider.java b/client/trino-client/src/main/java/io/trino/client/auth/kerberos/DelegatedConstrainedContextProvider.java new file mode 100644 index 000000000000..01c7393eb774 --- /dev/null +++ b/client/trino-client/src/main/java/io/trino/client/auth/kerberos/DelegatedConstrainedContextProvider.java @@ -0,0 +1,44 @@ +/* + * 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.client.auth.kerberos; + +import io.trino.client.ClientException; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; + +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class DelegatedConstrainedContextProvider + extends BaseGSSContextProvider +{ + private final GSSCredential gssCredential; + + public DelegatedConstrainedContextProvider(GSSCredential gssCredential) + { + this.gssCredential = requireNonNull(gssCredential, "gssCredential is null"); + } + + @Override + public GSSContext getContext(String servicePrincipal) + throws GSSException + { + if (gssCredential.getRemainingLifetime() < MIN_CREDENTIAL_LIFETIME.getValue(SECONDS)) { + throw new ClientException(format("Kerberos credential is expired: %s seconds", gssCredential.getRemainingLifetime())); + } + return createContext(servicePrincipal, gssCredential); + } +} diff --git a/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java b/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java index 548f01f3ff3d..5b3aaf114a35 100644 --- a/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java +++ b/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java @@ -21,6 +21,7 @@ import io.airlift.units.Duration; import io.trino.client.ClientSelectedRole; import io.trino.client.auth.external.ExternalRedirectStrategy; +import org.ietf.jgss.GSSCredential; import java.io.File; import java.util.List; @@ -77,6 +78,7 @@ enum SslVerificationMode public static final ConnectionProperty KERBEROS_KEYTAB_PATH = new KerberosKeytabPath(); public static final ConnectionProperty KERBEROS_CREDENTIAL_CACHE_PATH = new KerberosCredentialCachePath(); public static final ConnectionProperty KERBEROS_DELEGATION = new KerberosDelegation(); + public static final ConnectionProperty KERBEROS_CONSTRAINED_DELEGATION = new KerberosConstrainedDelegation(); public static final ConnectionProperty ACCESS_TOKEN = new AccessToken(); public static final ConnectionProperty EXTERNAL_AUTHENTICATION = new ExternalAuthentication(); public static final ConnectionProperty EXTERNAL_AUTHENTICATION_TIMEOUT = new ExternalAuthenticationTimeout(); @@ -120,6 +122,7 @@ enum SslVerificationMode .add(KERBEROS_KEYTAB_PATH) .add(KERBEROS_CREDENTIAL_CACHE_PATH) .add(KERBEROS_DELEGATION) + .add(KERBEROS_CONSTRAINED_DELEGATION) .add(ACCESS_TOKEN) .add(EXTRA_CREDENTIALS) .add(CLIENT_INFO) @@ -455,6 +458,11 @@ private static Predicate isKerberosWithoutDelegation() return isKerberosEnabled().and(checkedPredicate(properties -> !KERBEROS_DELEGATION.getValue(properties).orElse(false))); } + private static Predicate isKerberosWithDelegation() + { + return isKerberosEnabled().and(checkedPredicate(properties -> KERBEROS_DELEGATION.getValue(properties).orElse(false))); + } + private static class KerberosServicePrincipalPattern extends AbstractConnectionProperty { @@ -518,6 +526,15 @@ public KerberosDelegation() } } + private static class KerberosConstrainedDelegation + extends AbstractConnectionProperty + { + public KerberosConstrainedDelegation() + { + super("KerberosConstrainedDelegation", Optional.empty(), NOT_REQUIRED, isKerberosWithDelegation(), GSSCredential.class::cast); + } + } + private static class AccessToken extends AbstractConnectionProperty { diff --git a/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java b/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java index 6d46e3224262..84e282211128 100644 --- a/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java +++ b/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java @@ -69,6 +69,7 @@ import static io.trino.jdbc.ConnectionProperties.HOSTNAME_IN_CERTIFICATE; import static io.trino.jdbc.ConnectionProperties.HTTP_PROXY; import static io.trino.jdbc.ConnectionProperties.KERBEROS_CONFIG_PATH; +import static io.trino.jdbc.ConnectionProperties.KERBEROS_CONSTRAINED_DELEGATION; import static io.trino.jdbc.ConnectionProperties.KERBEROS_CREDENTIAL_CACHE_PATH; import static io.trino.jdbc.ConnectionProperties.KERBEROS_DELEGATION; import static io.trino.jdbc.ConnectionProperties.KERBEROS_KEYTAB_PATH; @@ -321,7 +322,8 @@ public void setupClient(OkHttpClient.Builder builder) KERBEROS_KEYTAB_PATH.getValue(properties), Optional.ofNullable(KERBEROS_CREDENTIAL_CACHE_PATH.getValue(properties) .orElseGet(() -> defaultCredentialCachePath().map(File::new).orElse(null))), - KERBEROS_DELEGATION.getRequiredValue(properties)); + KERBEROS_DELEGATION.getRequiredValue(properties), + KERBEROS_CONSTRAINED_DELEGATION.getValue(properties)); } if (ACCESS_TOKEN.getValue(properties).isPresent()) { @@ -486,7 +488,8 @@ private static Properties mergeConnectionProperties(URI uri, Properties driverPr { Map defaults = ConnectionProperties.getDefaults(); Map urlProperties = parseParameters(uri.getQuery()); - Map suppliedProperties = driverProperties.entrySet().stream().collect(toImmutableMap(entry -> (String) entry.getKey(), Entry::getValue)); + Map suppliedProperties = driverProperties.entrySet().stream() + .collect(toImmutableMap(entry -> (String) entry.getKey(), Entry::getValue)); for (String key : urlProperties.keySet()) { if (suppliedProperties.containsKey(key)) { diff --git a/docs/src/main/sphinx/client/jdbc.rst b/docs/src/main/sphinx/client/jdbc.rst index 6bfe985d28cd..20fb893cfc31 100644 --- a/docs/src/main/sphinx/client/jdbc.rst +++ b/docs/src/main/sphinx/client/jdbc.rst @@ -185,6 +185,7 @@ Name Description ``KerberosDelegation`` Set to ``true`` to use the token from an existing Kerberos context. This allows client to use Kerberos authentication without passing the Keytab or credential cache. Defaults to ``false``. +``KerberosConstrainedDelegation`` Pass GssCredential object as driver property directly to driver. ``extraCredentials`` Extra credentials for connecting to external services, specified as a list of key-value pairs. For example, ``foo:bar;abc:xyz`` creates the credential named ``abc`` diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite1.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite1.java index f663bdcf45ca..b13727846468 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite1.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite1.java @@ -36,6 +36,7 @@ public List getTestRuns(EnvironmentConfig config) "cli", "jdbc", "trino_jdbc", + "jdbc_kerberos_constrained_delegation", "functions", "hive_compression", "large_query", diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite3.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite3.java index eeec64f77a6e..dff21ceec753 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite3.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite3.java @@ -49,7 +49,7 @@ public List getTestRuns(EnvironmentConfig config) .withTests("TestHiveStorageFormats.testOrcTableCreatedInTrino", "TestHiveCreateTable.testCreateTable") .build(), testOnEnvironment(EnvMultinodeTlsKerberosDelegation.class) - .withGroups("configured_features", "jdbc") + .withGroups("configured_features", "jdbc", "jdbc_kerberos_constrained_delegation") .build()); } } diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java index 6a469d607bb7..9bb8110f04a1 100644 --- a/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java @@ -28,6 +28,7 @@ public final class TestGroups public static final String BLACKHOLE_CONNECTOR = "blackhole"; public static final String SMOKE = "smoke"; public static final String JDBC = "jdbc"; + public static final String JDBC_KERBEROS_CONSTRAINED_DELEGATION = "jdbc_kerberos_constrained_delegation"; public static final String OAUTH2 = "oauth2"; public static final String OAUTH2_REFRESH = "oauth2_refresh"; public static final String MYSQL = "mysql"; diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/jdbc/TestKerberosConstrainedDelegationJdbc.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/jdbc/TestKerberosConstrainedDelegationJdbc.java new file mode 100644 index 000000000000..c41813c5c57c --- /dev/null +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/jdbc/TestKerberosConstrainedDelegationJdbc.java @@ -0,0 +1,165 @@ +/* + * 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.tests.product.jdbc; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import io.trino.tempto.BeforeMethodWithContext; +import io.trino.tempto.ProductTest; +import io.trino.tempto.kerberos.KerberosAuthentication; +import io.trino.tests.product.TpchTableResults; +import org.assertj.core.api.Assertions; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; +import org.testng.annotations.Test; + +import javax.security.auth.Subject; + +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.trino.tempto.assertions.QueryAssert.assertThat; +import static io.trino.tempto.query.QueryResult.forResultSet; +import static io.trino.tests.product.TestGroups.JDBC_KERBEROS_CONSTRAINED_DELEGATION; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.ietf.jgss.GSSCredential.DEFAULT_LIFETIME; +import static org.ietf.jgss.GSSCredential.INITIATE_ONLY; +import static org.ietf.jgss.GSSName.NT_USER_NAME; + +public class TestKerberosConstrainedDelegationJdbc + extends ProductTest +{ + private static final String KERBEROS_OID = "1.2.840.113554.1.2.2"; + @Inject + @Named("databases.presto.jdbc_url") + String jdbcUrl; + + @Inject + @Named("databases.presto.kerberos_principal") + private String kerberosPrincipal; + + @Inject + @Named("databases.presto.kerberos_keytab") + private String kerberosKeytab; + + private GSSManager gssManager; + private KerberosAuthentication kerberosAuthentication; + + @BeforeMethodWithContext + public void setUp() + { + this.gssManager = GSSManager.getInstance(); + this.kerberosAuthentication = new KerberosAuthentication(kerberosPrincipal, kerberosKeytab); + } + + @Test(groups = JDBC_KERBEROS_CONSTRAINED_DELEGATION) + public void testSelectConstrainedDelegationKerberos() + throws Exception + { + Properties driverProperties = new Properties(); + GSSCredential credential = createGssCredential(); + driverProperties.put("KerberosConstrainedDelegation", credential); + try (Connection connection = DriverManager.getConnection(jdbcUrl, driverProperties); + PreparedStatement statement = connection.prepareStatement("SELECT * FROM tpch.tiny.nation"); + ResultSet results = statement.executeQuery()) { + assertThat(forResultSet(results)).matches(TpchTableResults.PRESTO_NATION_RESULT); + } + finally { + credential.dispose(); + } + } + + @Test(groups = JDBC_KERBEROS_CONSTRAINED_DELEGATION) + public void testCtasConstrainedDelegationKerberos() + throws Exception + { + Properties driverProperties = new Properties(); + GSSCredential credential = createGssCredential(); + driverProperties.put("KerberosConstrainedDelegation", credential); + try (Connection connection = DriverManager.getConnection(jdbcUrl, driverProperties); + PreparedStatement statement = connection.prepareStatement(format("CREATE TABLE %s AS SELECT * FROM tpch.tiny.nation", "test_kerberos_ctas"))) { + int results = statement.executeUpdate(); + Assertions.assertThat(results).isEqualTo(25); + } + finally { + credential.dispose(); + } + } + + @Test(groups = JDBC_KERBEROS_CONSTRAINED_DELEGATION) + public void testQueryOnDisposedCredential() + throws Exception + { + Properties driverProperties = new Properties(); + GSSCredential credential = createGssCredential(); + credential.dispose(); + driverProperties.put("KerberosConstrainedDelegation", credential); + try (Connection connection = DriverManager.getConnection(jdbcUrl, driverProperties)) { + assertThatThrownBy(() -> connection.prepareStatement("SELECT * FROM tpch.tiny.nation")) + .cause() + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("This credential is no longer valid"); + } + } + + @Test(groups = JDBC_KERBEROS_CONSTRAINED_DELEGATION) + public void testQueryOnExpiredCredential() + throws Exception + { + Properties driverProperties = new Properties(); + GSSCredential credential = createGssCredential(); + // ticket default lifetime is 80s by kerb.conf, sleep to expire ticket + Thread.sleep(30000); + // check before execution that current lifetime is less than 60s (MIN_LIFETIME on client), to be sure that we already expired + Assertions.assertThat(credential.getRemainingLifetime()).isLessThanOrEqualTo(60); + driverProperties.put("KerberosConstrainedDelegation", credential); + try (Connection connection = DriverManager.getConnection(jdbcUrl, driverProperties)) { + assertThatThrownBy(() -> connection.prepareStatement("SELECT * FROM tpch.tiny.nation")) + .isInstanceOf(SQLException.class) + .hasMessageContaining("Kerberos credential is expired"); + } + finally { + credential.dispose(); + } + } + + private GSSCredential createGssCredential() + { + Subject authenticatedSubject = this.kerberosAuthentication.authenticate(); + Principal clientPrincipal = getOnlyElement(authenticatedSubject.getPrincipals()); + + try { + return Subject.doAs(authenticatedSubject, + (PrivilegedExceptionAction) () -> + gssManager.createCredential( + gssManager.createName(clientPrincipal.getName(), NT_USER_NAME), + DEFAULT_LIFETIME, + new Oid(KERBEROS_OID), + INITIATE_ONLY)); + } + catch (PrivilegedActionException e) { + throw new RuntimeException(e.getCause()); + } + } +}