From f82390894bd29d20cbaa695eb34dcdecce697944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 12 Jun 2019 10:27:58 +0200 Subject: [PATCH] Bugfix/196 reenable kerberos in import (#206) * #196: Reenabled Kerberos authentication in `IMPORT` --- README.md | 6 +- .../jdbc/BaseConnectionDefinitionBuilder.java | 8 +- .../exasol/adapter/jdbc/KerberosUtils.java | 78 ----------- .../adapter/jdbc/RemoteConnectionFactory.java | 59 ++++++--- .../KerberosConfigurationCreator.java | 121 ++++++++++++++++++ ...KerberosConfigurationCreatorException.java | 27 ++++ .../dialects/SqlDialectRegistryTest.java | 2 +- .../jdbc/DummyRemoteMetadataReader.java | 6 +- .../jdbc/RemoteConnectionFactoryTest.java | 44 +++++-- .../KerberosConfigurationCreatorTest.java | 107 ++++++++++++++++ .../exasol/logging/CompactFormatterTest.java | 2 +- 11 files changed, 342 insertions(+), 118 deletions(-) delete mode 100644 jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/KerberosUtils.java create mode 100644 jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreator.java create mode 100644 jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorException.java create mode 100644 jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorTest.java diff --git a/README.md b/README.md index 639e21b05..7debb4733 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,12 @@ Running the Virtual Schema requires a Java Runtime version 8 or later. | Dependency | Purpose | License | |-------------------------------------------------------------------------------------|--------------------------------------------------------|-------------------------------| | [Apache Derby](https://db.apache.org/derby/) | Pure-Java embedded database | Apache License 2.0 | +| [Apache HTTP Components](http://hc.apache.org/) | HTTP communication | Apache License 2.0 | | [Apache Maven](https://maven.apache.org/) | Build tool | Apache License 2.0 | +| [Equals Verifier](https://jqno.nl/equalsverifier/) | Testing `equals(...)` and `hashCode()` contracts | Apache License 2.0 | | [Java Hamcrest](http://hamcrest.org/JavaHamcrest/) | Checking for conditions in code via matchers | BSD License | | [JSONassert](http://jsonassert.skyscreamer.org/) | Compare JSON documents for semantic equality | Apache License 2.0 | | [JUnit](https://junit.org/junit5) | Unit testing framework | Eclipse Public License 1.0 | -| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License | | [JUnit 5 System Extensions](https://github.com/itsallcode/junit5-system-extensions) | Capturing `STDOUT` and `STDERR` | Eclipse Public License 2.0 | -| [Equals Verifier](https://jqno.nl/equalsverifier/) | Testing `equals(...)` and `hashCode()` contracts | Apache License 2.0 | \ No newline at end of file +| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License | +| [SnakeYaml](https://bitbucket.org/asomov/snakeyaml/src/default/) | YAML parsing | Apache License 2.0 | \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/BaseConnectionDefinitionBuilder.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/BaseConnectionDefinitionBuilder.java index 31c159d00..9730daf3f 100644 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/BaseConnectionDefinitionBuilder.java +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/BaseConnectionDefinitionBuilder.java @@ -43,10 +43,6 @@ public String buildConnectionDefinition(final AdapterProperties properties, } } - private String getNamedConnection(final AdapterProperties properties) { - return "AT " + properties.getConnectionName(); - } - private String getConnectionFromPropertiesOnly(final AdapterProperties properties) { warnConnectionPropertiesDeprecated(); return getConnectionDefinition(properties.getConnectionString(), properties.getUsername(), @@ -72,4 +68,8 @@ protected boolean hasConflictingConnectionProperties(final AdapterProperties pro return properties.hasConnectionName() && (properties.hasConnectionString() || properties.hasUsername() || properties.hasPassword()); } + + private String getNamedConnection(final AdapterProperties properties) { + return "AT " + properties.getConnectionName(); + } } \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/KerberosUtils.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/KerberosUtils.java deleted file mode 100644 index 6eca52689..000000000 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/KerberosUtils.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.exasol.adapter.jdbc; - -import javax.xml.bind.DatatypeConverter; -import java.io.File; -import java.io.FileOutputStream; -import java.nio.charset.Charset; - -/** - * Utility class to establish JDBC connections with Kerberos authentication - */ -public class KerberosUtils { - - private static final String KRB_KEY = "ExaAuthType=Kerberos;"; - - public static boolean isKerberosAuth(final String pass) { - if (pass == null) { - return false; - } - return pass.indexOf(KRB_KEY) == 0; - } - - public static void configKerberos(final String user, String pass) throws Exception { - try { - pass = pass.replaceFirst(KRB_KEY, ""); - } catch (final Exception e) { - throw new RuntimeException("Could not find " + KRB_KEY + " in password: " + e.getMessage()); - } - final String[] confKeytab = pass.split(";"); - if (confKeytab.length != 2) - throw new RuntimeException("Invalid Kerberos conf/keytab"); - final File kerberosBaseDir = new File("/tmp"); - final File krbDir = File.createTempFile("kerberos_", null, kerberosBaseDir); - krbDir.delete(); - krbDir.mkdir(); - krbDir.deleteOnExit(); - final String krbConfPath = writePath(krbDir, confKeytab[0], "krb_", ".conf"); - final String keytabPath = writePath(krbDir, confKeytab[1], "kt_", ".keytab"); - final String jaasConfigPath = writeJaasConfig(krbDir, user, keytabPath); - System.setProperty("java.security.auth.login.config", jaasConfigPath); - System.setProperty("java.security.krb5.conf", krbConfPath); - System.setProperty("javax.security.auth.useSubjectCredsOnly", "false"); - } - - private static String writePath(final File krbDir, final String confKeyTab, final String prefix, final String suffix) throws Exception { - final File file = File.createTempFile(prefix, suffix, krbDir); - file.deleteOnExit(); - try (final FileOutputStream os = new FileOutputStream(file);) { - os.write(DatatypeConverter.parseBase64Binary(confKeyTab)); - } - return file.getCanonicalPath(); - } - - private static String writeJaasConfig(final File krbDir, final String princ, final String keytabPath) throws Exception { - final File file = File.createTempFile("jaas_", ".conf", krbDir); - file.deleteOnExit(); - String jaasData; - jaasData = "Client {\n"; - jaasData += "com.sun.security.auth.module.Krb5LoginModule required\n"; - jaasData += "principal=\"" + princ + "\"\n"; - jaasData += "useKeyTab=true\n"; - jaasData += "keyTab=\"" + keytabPath + "\"\n"; - jaasData += "doNotPrompt=true\n"; - jaasData += "useTicketCache=false;\n"; - jaasData += "};\n"; - jaasData += "com.sun.security.jgss.initiate {\n"; - jaasData += "com.sun.security.auth.module.Krb5LoginModule required\n"; - jaasData += "principal=\"" + princ + "\"\n"; - jaasData += "useKeyTab=true\n"; - jaasData += "keyTab=\"" + keytabPath + "\"\n"; - jaasData += "doNotPrompt=true\n"; - jaasData += "useTicketCache=false;\n"; - jaasData += "};\n"; - try (final FileOutputStream os = new FileOutputStream(file)) { - os.write(jaasData.getBytes(Charset.forName("UTF-8"))); - } - return file.getCanonicalPath(); - } -} diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/RemoteConnectionFactory.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/RemoteConnectionFactory.java index acb46a1d6..01987d241 100644 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/RemoteConnectionFactory.java +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/adapter/jdbc/RemoteConnectionFactory.java @@ -1,14 +1,12 @@ package com.exasol.adapter.jdbc; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; +import java.sql.*; +import java.util.Properties; import java.util.logging.Logger; -import com.exasol.ExaConnectionAccessException; -import com.exasol.ExaConnectionInformation; -import com.exasol.ExaMetadata; +import com.exasol.*; import com.exasol.adapter.AdapterProperties; +import com.exasol.auth.kerberos.KerberosConfigurationCreator; /** * Factory that produces JDBC connections to remote data sources @@ -19,12 +17,10 @@ public class RemoteConnectionFactory { /** * Create a JDBC connection to the remote data source * - * @param exaMetadata Exasol metadata (contains information about stored * - * connection details) + * @param exaMetadata Exasol metadata (contains information about stored * connection details) * @param properties user-defined adapter properties * @return JDBC connection to remote data source - * @throws SQLException if the connection to the remote source could not be - * established + * @throws SQLException if the connection to the remote source could not be established */ public Connection createConnection(final ExaMetadata exaMetadata, final AdapterProperties properties) throws SQLException { @@ -45,8 +41,9 @@ private Connection createConnectionWithUserCredentials(final String username, fi return connection; } - protected void logConnectionAttempt(final String address, final String user) { - LOGGER.fine(() -> "Connecting to \"" + address + "\" as user \"" + user + "\""); + protected void logConnectionAttempt(final String address, final String username) { + LOGGER.fine( + () -> "Connecting to \"" + address + "\" as user \"" + username + "\" using password authentication."); } protected void logRemoteDatabaseDetails(final Connection connection) throws SQLException { @@ -59,15 +56,43 @@ private Connection createConnection(final String connectionName, final ExaMetada throws SQLException { try { final ExaConnectionInformation exaConnection = exaMetadata.getConnection(connectionName); + final String password = exaConnection.getPassword(); final String username = exaConnection.getUser(); final String address = exaConnection.getAddress(); - logConnectionAttempt(address, username); - final Connection connection = DriverManager.getConnection(address, username, exaConnection.getPassword()); - logRemoteDatabaseDetails(connection); - return connection; + if (KerberosConfigurationCreator.isKerberosAuthentication(password)) { + return establishConnectionWithKerberos(password, username, address); + } else { + return establishConnectionWithRegularCredentials(password, username, address); + } } catch (final ExaConnectionAccessException exception) { throw new RemoteConnectionException( - "Could not access the connection information of connection \"" + connectionName + "\"", exception); + "Could not access the connection information of connection \"" + connectionName + "\".", exception); } } + + private Connection establishConnectionWithKerberos(final String password, final String username, + final String address) throws SQLException { + logConnectionAttemptWithKerberos(address, username); + final Properties jdbcProperties = new Properties(); + jdbcProperties.put("user", username); + jdbcProperties.put("password", password); + final KerberosConfigurationCreator kerberosConfigurationCreator = new KerberosConfigurationCreator(); + kerberosConfigurationCreator.writeKerberosConfigurationFiles(username, password); + final Connection connection = DriverManager.getConnection(address, jdbcProperties); + logRemoteDatabaseDetails(connection); + return connection; + } + + private void logConnectionAttemptWithKerberos(final String address, final String username) { + LOGGER.fine( + () -> "Connecting to \"" + address + "\" as user \"" + username + "\" using Kerberos authentication."); + } + + private Connection establishConnectionWithRegularCredentials(final String password, final String username, + final String address) throws SQLException { + logConnectionAttempt(address, username); + final Connection connection = DriverManager.getConnection(address, username, password); + logRemoteDatabaseDetails(connection); + return connection; + } } diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreator.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreator.java new file mode 100644 index 000000000..a012ae5ea --- /dev/null +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreator.java @@ -0,0 +1,121 @@ +package com.exasol.auth.kerberos; + +import static javax.xml.bind.DatatypeConverter.parseBase64Binary; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Logger; + +/** + * This class generates the necessary configuration for a successful Kerberos authentication. + * + * @see Kerberos authentication for + * IMPORT (Exasol online documentation) + */ +public class KerberosConfigurationCreator { + public static final String USE_SUBJECT_CREDENTIALS_ONLY_PROPERTY = "javax.security.auth.useSubjectCredsOnly"; + public static final String KERBEROS_CONFIG_PROPERTY = "java.security.krb5.conf"; + public static final String LOGIN_CONFIG_PROPERTY = "java.security.auth.login.config"; + public static final String KERBEROS_AUTHENTICATION_PREAMBLE = "ExaAuthType=Kerberos"; + private static final Logger LOGGER = Logger.getLogger(KerberosConfigurationCreator.class.getName()); + + /** + * Check whether the given password contains Kerberos credentials + * + * @param password password / credential string to be examined + * @return true if the password is a Kerberos credential string, false otherwise + */ + public static boolean isKerberosAuthentication(final String password) { + return password.startsWith(KERBEROS_AUTHENTICATION_PREAMBLE); + } + + /** + * Create Kerberos configuration and system properties + * + * @param user Kerberos principal + * @param password connection password containing kerberos configuration and key tab + */ + public void writeKerberosConfigurationFiles(final String user, final String password) { + final String[] tokens = password.split(";"); + final String preamble = tokens[0]; + if ((tokens.length == 3) && KERBEROS_AUTHENTICATION_PREAMBLE.equals(preamble)) { + final String base64EncodedKerberosConfig = tokens[1]; + final String base64EncodedKeyTab = tokens[2]; + createKerberosConfiguration(user, base64EncodedKerberosConfig, base64EncodedKeyTab); + } else { + throw new KerberosConfigurationCreatorException("Syntax error in Kerberos password." + + " Must conform to: 'ExaAuthType=Kerberos;;'"); + } + } + + private void createKerberosConfiguration(final String user, final String base64EncodedKerberosConfig, + final String base64EncodedKeyTab) { + try { + final Path temporaryDirectory = createCommonDirectoryForKerberosConfigurationFiles(); + final Path kerberosConfigPath = createTemporaryKerberosConfigFile(base64EncodedKerberosConfig, + temporaryDirectory); + final Path keyTabPath = createTemporaryKeyTabFile(base64EncodedKeyTab, temporaryDirectory); + final Path jaasConfigPath = createTemporaryJaasConfig(temporaryDirectory, user, keyTabPath); + setKerberosSystemProperties(kerberosConfigPath, jaasConfigPath); + } catch (final IOException exception) { + throw new KerberosConfigurationCreatorException("Unable to create temporary Kerberos configuration file.", + exception); + } + } + + private Path createCommonDirectoryForKerberosConfigurationFiles() throws IOException { + final Path temporaryDirectory = Files.createTempDirectory("kerberos_"); + temporaryDirectory.toFile().deleteOnExit(); + LOGGER.finer(() -> "Created temporary directory \"" + temporaryDirectory + + "\" to contain Kerberos authentication files."); + return temporaryDirectory; + } + + private Path createTemporaryKerberosConfigFile(final String base64EncodedKerberosConfig, + final Path temporaryDirectory) throws IOException { + return createTemporaryFile(temporaryDirectory, "krb_", ".conf", parseBase64Binary(base64EncodedKerberosConfig)); + } + + private Path createTemporaryKeyTabFile(final String base64EncodedKeyTab, final Path temporaryDirectory) + throws IOException { + return createTemporaryFile(temporaryDirectory, "kt_", ".keytab", parseBase64Binary(base64EncodedKeyTab)); + } + + private Path createTemporaryFile(final Path temporaryDirectory, final String prefix, final String suffix, + final byte[] content) throws IOException { + final Path temporaryFile = Files.createTempFile(temporaryDirectory, prefix, suffix); + temporaryFile.toFile().deleteOnExit(); + Files.write(temporaryFile, content); + LOGGER.finer( + () -> "Wrote " + content.length + " bytes to Kerberos configuration file \"" + temporaryFile + "\"."); + return temporaryFile; + } + + private Path createTemporaryJaasConfig(final Path temporaryDirectory, final String user, final Path keyTabPath) + throws IOException { + final byte[] content = ("Client {\n" // + + "com.sun.security.auth.module.Krb5LoginModule required\n" // + + "principal=\"" + user + "\"\n" // + + "useKeyTab=true\n" // + + "keyTab=\"" + keyTabPath + "\"\n" // + + "doNotPrompt=true\n" // + + "useTicketCache=false;\n" // + + "};\n" // + + "com.sun.security.jgss.initiate {\n" // + + "com.sun.security.auth.module.Krb5LoginModule required\n" // + + "principal=\"" + user + "\"\n" // + + "useKeyTab=true\n" // + + "keyTab=\"" + keyTabPath + "\"\n" // + + "doNotPrompt=true\n" // + + "useTicketCache=false;\n" // + + "};\n").getBytes(); + return createTemporaryFile(temporaryDirectory, "jaas_", ".conf", content); + } + + private void setKerberosSystemProperties(final Path kerberosConfigPath, final Path jaasConfigPath) { + System.setProperty(KERBEROS_CONFIG_PROPERTY, kerberosConfigPath.toString()); + System.setProperty(LOGIN_CONFIG_PROPERTY, jaasConfigPath.toString()); + System.setProperty(USE_SUBJECT_CREDENTIALS_ONLY_PROPERTY, "false"); + } +} \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorException.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorException.java new file mode 100644 index 000000000..833a85cde --- /dev/null +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/main/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorException.java @@ -0,0 +1,27 @@ +package com.exasol.auth.kerberos; + +/** + * Exception for errors occurring during creation of the Kerberos configuration or its files + */ +public class KerberosConfigurationCreatorException extends RuntimeException { + private static final long serialVersionUID = -7910268166902081246L; + + /** + * Create a new instance of a {@link KerberosConfigurationCreatorException} + * + * @param message error message + */ + public KerberosConfigurationCreatorException(final String message) { + super(message); + } + + /** + * Create a new instance of a {@link KerberosConfigurationCreatorException} + * + * @param message error message + * @param cause cause + */ + public KerberosConfigurationCreatorException(final String message, final Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/dialects/SqlDialectRegistryTest.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/dialects/SqlDialectRegistryTest.java index 98946d631..8e6a9b67e 100644 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/dialects/SqlDialectRegistryTest.java +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/dialects/SqlDialectRegistryTest.java @@ -1,7 +1,7 @@ package com.exasol.adapter.dialects; -import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.Matchers.*; +import static org.hamcrest.text.MatchesPattern.matchesPattern; import static org.junit.Assert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/DummyRemoteMetadataReader.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/DummyRemoteMetadataReader.java index 01db1317a..d96080e14 100644 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/DummyRemoteMetadataReader.java +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/DummyRemoteMetadataReader.java @@ -12,20 +12,16 @@ public DummyRemoteMetadataReader(final Connection connection, final AdapterPrope @Override protected ColumnMetadataReader createColumnMetadataReader() { - // TODO Auto-generated method stub return null; } @Override protected TableMetadataReader createTableMetadataReader() { - // TODO Auto-generated method stub return null; } @Override protected IdentifierConverter createIdentifierConverter() { - // TODO Auto-generated method stub return null; } - -} +} \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/RemoteConnectionFactoryTest.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/RemoteConnectionFactoryTest.java index 7de558b6a..a3e5bca86 100644 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/RemoteConnectionFactoryTest.java +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/adapter/jdbc/RemoteConnectionFactoryTest.java @@ -1,7 +1,11 @@ package com.exasol.adapter.jdbc; +import static com.exasol.auth.kerberos.KerberosConfigurationCreator.KERBEROS_CONFIG_PROPERTY; +import static com.exasol.auth.kerberos.KerberosConfigurationCreator.LOGIN_CONFIG_PROPERTY; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.matchesPattern; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @@ -10,6 +14,8 @@ import java.util.HashMap; import java.util.Map; +import javax.xml.bind.DatatypeConverter; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +30,8 @@ @MockitoSettings(strictness = Strictness.LENIENT) @ExtendWith(MockitoExtension.class) class RemoteConnectionFactoryTest { + private static final String CONNECTION_NAME = "testConnection"; + private static final String DERBY_INSTANT_JDBC_CONNECTION_STRING = "jdbc:derby:memory:test;create=true;"; private static final String USER = "testUserName"; private Map rawProperties; @@ -39,24 +47,23 @@ void beforeEach() { @Test void testCreateConnectionWithConnectionName() throws ExaConnectionAccessException, SQLException { - this.rawProperties.put("CONNECTION_NAME", "testConnection"); - when(this.exaMetadataMock.getConnection("testConnection")).thenReturn(this.exaConnectionMock); + this.rawProperties.put("CONNECTION_NAME", CONNECTION_NAME); + when(this.exaMetadataMock.getConnection(CONNECTION_NAME)).thenReturn(this.exaConnectionMock); when(this.exaConnectionMock.getUser()).thenReturn(USER); when(this.exaConnectionMock.getPassword()).thenReturn("pass"); - when(this.exaConnectionMock.getAddress()).thenReturn("jdbc:derby:memory:test;create=true;"); + when(this.exaConnectionMock.getAddress()).thenReturn(DERBY_INSTANT_JDBC_CONNECTION_STRING); assertThat(createConnection().getMetaData().getUserName(), equalTo(USER)); } private Connection createConnection() throws SQLException { final RemoteConnectionFactory factory = new RemoteConnectionFactory(); - final Connection connection = factory.createConnection(this.exaMetadataMock, - new AdapterProperties(this.rawProperties)); - return connection; + final AdapterProperties properties = new AdapterProperties(this.rawProperties); + return factory.createConnection(this.exaMetadataMock, properties); } @Test void testCreateConnectionWithConnectionDetailsInProperties() throws ExaConnectionAccessException, SQLException { - this.rawProperties.put("CONNECTION_STRING", "jdbc:derby:memory:test;create=true;"); + this.rawProperties.put("CONNECTION_STRING", DERBY_INSTANT_JDBC_CONNECTION_STRING); this.rawProperties.put("USERNAME", USER); this.rawProperties.put("PASSWORD", "testPassword"); assertThat(createConnection().getMetaData().getUserName(), equalTo(USER)); @@ -66,7 +73,7 @@ void testCreateConnectionWithConnectionDetailsInProperties() throws ExaConnectio void testCreateConnectionWithConnectionDetailsInPropertiesAndEmptyConnectionName() throws ExaConnectionAccessException, SQLException { this.rawProperties.put("CONNECTION_NAME", ""); - this.rawProperties.put("CONNECTION_STRING", "jdbc:derby:memory:test;create=true;"); + this.rawProperties.put("CONNECTION_STRING", DERBY_INSTANT_JDBC_CONNECTION_STRING); this.rawProperties.put("USERNAME", USER); this.rawProperties.put("PASSWORD", "testPassword"); assertThat(createConnection().getMetaData().getUserName(), equalTo(USER)); @@ -75,9 +82,26 @@ void testCreateConnectionWithConnectionDetailsInPropertiesAndEmptyConnectionName @Test void testCreateConnectionWithUnaccessibleNamedConnectionThrowsException() throws ExaConnectionAccessException, SQLException { - when(this.exaMetadataMock.getConnection("testConnection")) + when(this.exaMetadataMock.getConnection(CONNECTION_NAME)) .thenThrow(new ExaConnectionAccessException("FAKE connection access exception")); - this.rawProperties.put("CONNECTION_NAME", "testConnection"); + this.rawProperties.put("CONNECTION_NAME", CONNECTION_NAME); assertThrows(RemoteConnectionException.class, () -> createConnection()); } + + @Test + void testCreateConnectionWithKerberosDetailsInNamedConnection() throws SQLException, ExaConnectionAccessException { + final String principal = "the_kerberos_principal"; + final String base64EncodedKerberosConfig = DatatypeConverter.printBase64Binary("".getBytes()); + final String base64EncodedKeyTab = DatatypeConverter.printBase64Binary("".getBytes()); + final String credentialString = "ExaAuthType=Kerberos;" + base64EncodedKerberosConfig + ";" + + base64EncodedKeyTab; + when(this.exaConnectionMock.getUser()).thenReturn(principal); + when(this.exaConnectionMock.getPassword()).thenReturn(credentialString); + when(this.exaConnectionMock.getAddress()).thenReturn(DERBY_INSTANT_JDBC_CONNECTION_STRING); + when(this.exaMetadataMock.getConnection(CONNECTION_NAME)).thenReturn(this.exaConnectionMock); + this.rawProperties.put("CONNECTION_NAME", CONNECTION_NAME); + createConnection(); // return value ignored on purpose + assertAll(() -> assertThat(System.getProperty(KERBEROS_CONFIG_PROPERTY), matchesPattern(".*/krb_.*\\.conf")), + () -> assertThat(System.getProperty(LOGIN_CONFIG_PROPERTY), matchesPattern(".*/jaas_.*\\.conf"))); + } } \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorTest.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorTest.java new file mode 100644 index 000000000..f99a96878 --- /dev/null +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/auth/kerberos/KerberosConfigurationCreatorTest.java @@ -0,0 +1,107 @@ +package com.exasol.auth.kerberos; + +import static com.exasol.auth.kerberos.KerberosConfigurationCreator.*; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.io.FileMatchers.anExistingFile; +import static org.hamcrest.text.MatchesPattern.matchesPattern; +import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +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; + +class KerberosConfigurationCreatorTest { + private static final String KEY_TAB_NAME = "ktbname"; + private static final String KERBEROS_CONFIG_NAME = "kbcname"; + private static final String JAAS_CONFIG_PATTERN = ".*/jaas_.*\\.conf"; + private static final String KERBEROS_CONFIG_PATTERN = ".*/krb_.*\\.conf"; + private static final String USER = "kerberos_user"; + private static final String PW = "ExaAuthType=Kerberos;" + KEY_TAB_NAME + ";" + KERBEROS_CONFIG_NAME; + private KerberosConfigurationCreator creator; + + @BeforeEach + void beforeEach() { + this.creator = new KerberosConfigurationCreator(); + } + + @Test + void testIsKerberosAuthenticationTrue() { + assertThat(KerberosConfigurationCreator.isKerberosAuthentication(PW), equalTo(true)); + } + + @Test + void testIsKerberosAuthenticationFalse() { + assertThat(KerberosConfigurationCreator.isKerberosAuthentication("not a kerberose password"), equalTo(false)); + } + + @Test + void testWriteKerberosConfigurationFiles() { + this.creator.writeKerberosConfigurationFiles(USER, PW); + assertAll( // + () -> assertJaasConfigurationPathProperty(), // + () -> assertKerberosConfigurationPathProperty(), // + () -> assertUseSubjectCredentialsProperty(), // + () -> assertJaasConfigurationFileContent(getJaasConfigPathFromProperty()), // + () -> assertKerberosFileContent(), // + () -> assertKeyTableFileContent(getJaasConfigPathFromProperty())); + } + + private String getJaasConfigPathFromProperty() { + return System.getProperty(LOGIN_CONFIG_PROPERTY); + } + + private void assertJaasConfigurationPathProperty() { + assertThat("JAAS configuration path", getJaasConfigPathFromProperty(), matchesPattern(JAAS_CONFIG_PATTERN)); + } + + private void assertKerberosConfigurationPathProperty() { + assertThat("Kerberos configuration path", getKerberosConfigFromProperty(), // + matchesPattern(KERBEROS_CONFIG_PATTERN)); + } + + private String getKerberosConfigFromProperty() { + return System.getProperty(KERBEROS_CONFIG_PROPERTY); + } + + private void assertUseSubjectCredentialsProperty() { + assertThat("Use subject credentials", System.getProperty(USE_SUBJECT_CREDENTIALS_ONLY_PROPERTY), + equalTo("false")); + } + + private void assertJaasConfigurationFileContent(final String jaasConfigurationPath) throws IOException { + final String content = getJaasConfigContent(jaasConfigurationPath); + assertAll(() -> assertThat(content, startsWith("Client {")), // + () -> assertThat(content, containsString("principal=\"" + USER + "\""))); + } + + private String getJaasConfigContent(final String jaasConfigurationPath) throws IOException { + return new String(Files.readAllBytes(Paths.get(jaasConfigurationPath))); + } + + private void assertKerberosFileContent() { + assertThat(new File(getKerberosConfigFromProperty()), anExistingFile()); + } + + private void assertKeyTableFileContent(final String jaasConfigurationPath) throws IOException { + final String jaasConfigContent = getJaasConfigContent(jaasConfigurationPath); + String keyTabPath = jaasConfigContent.substring(jaasConfigContent.indexOf("keyTab=\"") + 8); + keyTabPath = keyTabPath.substring(0, keyTabPath.indexOf("\"")); + assertThat("Key tab file: " + keyTabPath, new File(keyTabPath), anExistingFile()); + } + + @ValueSource(strings = { "", "missing preamble;foo;bar", "ExaAuthType=Kerberos;missing next part", + "ExaAuthType=Kerberos;too;many;parts" }) + @ParameterizedTest + void testIllegalKerberosPasswordThrowsException(final String password) { + assertThrows(KerberosConfigurationCreatorException.class, + () -> this.creator.writeKerberosConfigurationFiles("anyone", password)); + } +} \ No newline at end of file diff --git a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/logging/CompactFormatterTest.java b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/logging/CompactFormatterTest.java index 667db2357..34d3ab697 100644 --- a/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/logging/CompactFormatterTest.java +++ b/jdbc-adapter/virtualschema-jdbc-adapter/src/test/java/com/exasol/logging/CompactFormatterTest.java @@ -1,6 +1,6 @@ package com.exasol.logging; -import static org.hamcrest.Matchers.matchesPattern; +import static org.hamcrest.text.MatchesPattern.matchesPattern; import static org.junit.Assert.assertThat; import java.util.logging.Level;