diff --git a/VERSION b/VERSION index 32447ce3..a87467fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.11.1 +5.11.2 diff --git a/documentation/design/datbase.md b/documentation/design/datbase.md index 2c21184a..29be8a15 100644 --- a/documentation/design/datbase.md +++ b/documentation/design/datbase.md @@ -23,11 +23,13 @@ These come in two flavors: Notes: 1. Uses DBNorIterator. +2. Resolves against the **user** connections, i.e. the `gor.db.credentials` file. See [Configuration](#configuration). #### **sql:// URIs** Notes: 1. Uses DBNorIterator (and SQLSource). +2. Resolves against the **user** connections, i.e. the `gor.db.credentials` file. See [Configuration](#configuration). ### **Limited SQL Commands** These come in flavors: @@ -50,6 +52,7 @@ gor db://rda:variant_annotations | top 10 Notes: 1. Uses DBSource and DbGenomicIterator. +2. Resolves against the **system** connections, i.e. the credentials the host passes in. See [Configuration](#configuration). #### **//db/ Paths** The //db/ paths are arbitrary SELECT statements that can be used to selected from a given database. @@ -68,7 +71,8 @@ Their limit is the can ONLY be executed from a link file but not from a GOR que Notes: 1. Uses DBNorIterator. -2. This is the old style of doing SQL access in GOR. The new preferred way is to use the `sql`, `norsql`, `gorsql` commands. This is likely to be deprecated. +2. Resolves against the **system** connections, i.e. the credentials the host passes in. See [Configuration](#configuration). +3. This is the old style of doing SQL access in GOR. The new preferred way is to use the `sql`, `norsql`, `gorsql` commands. This is likely to be deprecated. ### Sepcial Variables @@ -79,23 +83,58 @@ TBD ### Configuration -TBD +There are two connection caches, and in a server they are fed from **separate** sources: + +| Cache | Used by | Fed from | +|---|---|---| +| system connections | internal use (e.g. session management) and the access-controlled operations `db://`, `//db:` | credentials the host passes in | +| user connections | the user-available commands and sources `SQL`, `GORSQL`, `NORSQL`, `sql://` | the `gor.db.credentials` file | + +They are kept apart deliberately. System credentials typically rotate, and a file cannot hold rotating +credentials without going stale; equally, the rotating system credentials are not the ones user queries +should reach. + +In a console app there is no host to supply credentials, so both caches load from the file. + +#### The credentials file + +`gor.db.credentials` is tab-separated, with a header line: + +``` +name driver url user pwd +rda org.postgresql.Driver jdbc:postgresql://myurl.com:5432/csa rda mypass +``` + +The password column is optional. Lines starting with `#` are ignored. + +Its location defaults to the config directory and can be set with the `gor.db.credentials` system +property. + +A missing credentials file is an error. + +#### Credentials passed in by the host + +The host application supplies the system credentials as `DbCredentials`: + +```java +var systemCredentials = List.of(new DbCredentials("rda", url, user, password)); + +DbConnection.initInServer(systemCredentials); +// which is, per cache: +DbConnection.systemConnections.initializeDbSources(systemCredentials); +DbConnection.userConnections.initializeDbSources(credpath); +``` + +Gor never reads credentials from the environment itself, and does not care where the host got them — +its own configuration, a secret manager, or environment variables the host owns the naming of. This +keeps deployment-specific naming out of the library. + +`DbCredentials` takes `name`, `url`, `user`, `pwd`, and an optional `driver`. When `driver` is null it +is derived from the url prefix (`jdbc:postgresql:`, `jdbc:oracle:`). Blank values count as unset, +including the password, so a secret manager rendering an empty string does not become a real empty +password. Incomplete credentials are logged — naming the missing field, never a value — and skipped, +rather than failing startup. -We have two different configuration files: -* -*
  • gor.db.credentials - contains the system databases, which are used by the system internally (e.g. session management), -* and by operation where we have strict access controls (db://, //db:). These credentials -* typically would grant full access to the database. -*
  • gor.sql.credentials - contains the user databases, which are used by the user available commands/sources (SQL, -* GORSQL, NORSQL, sql://). These credentials typically would grant limited/read-only access to -* the database. -*


    -* The format for these files is: -*

    -*    name\tdriver\turl\tuser\tpwd
    -*    rda\torg.postgresql.Driver\tjdbc:postgresql://myurl.com:5432/csa\trda\tmypass
    -*    ...
    -* 
    -*

    -* The location of the files defaults to the config directory but can be specified by the system properties -* gor.db.credentials and gor.sql.credentials. \ No newline at end of file +The two `initializeDbSources` overloads are alternatives, not additive: each clears the cache first. +That is what keeps a cache fed from exactly one source. Passing no credentials therefore leaves the +system cache empty, and `db://` sources will not resolve. \ No newline at end of file diff --git a/documentation/src/command/GORSQL.rst b/documentation/src/command/GORSQL.rst index a2085790..a1b7f120 100644 --- a/documentation/src/command/GORSQL.rst +++ b/documentation/src/command/GORSQL.rst @@ -7,7 +7,7 @@ ====== GORSQL ====== -The :ref:`GORSQL` command allows you to run arbitrary SQL commands against "the database" (the database here being defined by the content of a file called gor.sql.credentials in the config directory). +The :ref:`GORSQL` command allows you to run arbitrary SQL commands against "the database" (the databases available here are the user connections, defined by the content of a file called gor.db.credentials in the config directory). For :ref:`GORSQL` to work properly, the defined query must return Chrom-POS information as first two columns. @@ -33,7 +33,7 @@ Options | ``-ff File`` | Read tags from a tag file and filter files and file contents on. Also accepts a nested query. | | | The following place holders are provided: #{TAGS}. The necessary quoting is done automatically. | +----------------------+---------------------------------------------------------------------------------------------------+ -| ``-db database`` | Database alias as defied in ``gor.sql.credentials``. | +| ``-db database`` | Database alias as defied in ``gor.db.credentials``. | +----------------------+---------------------------------------------------------------------------------------------------+ diff --git a/documentation/src/command/NORSQL.rst b/documentation/src/command/NORSQL.rst index 2d6449ec..7d80d017 100644 --- a/documentation/src/command/NORSQL.rst +++ b/documentation/src/command/NORSQL.rst @@ -7,7 +7,7 @@ ====== NORSQL ====== -The :ref:`NORSQL` command allows you to run arbitrary SQL commands against "the database" (the database here being defined by the content of a file called gor.sql.credentials in the config directory). +The :ref:`NORSQL` command allows you to run arbitrary SQL commands against "the database" (the databases available here are the user connections, defined by the content of a file called gor.db.credentials in the config directory). :ref:`NORSQL` can be run against any database table. diff --git a/documentation/src/command/SQL.rst b/documentation/src/command/SQL.rst index bd71cdf5..257143d4 100644 --- a/documentation/src/command/SQL.rst +++ b/documentation/src/command/SQL.rst @@ -7,7 +7,7 @@ === SQL === -The SQL command allows you to run arbitrary SQL commands against "the database" (the database here being defined by the content of a file called gor.sql.credentials in the config directory). +The SQL command allows you to run arbitrary SQL commands against "the database" (the databases available here are the user connections, defined by the content of a file called gor.db.credentials in the config directory). SQL statements need to be encapsulated between curly brackets, e.g. sql {sql_query}. @@ -37,7 +37,7 @@ Options | ``-ff File`` | Read tags from a tag file and filter files and file contents on. Also accepts a nested query. | | | The following place holders are provided: #{TAGS}. The necessary quoting is done automatically. | +----------------------+---------------------------------------------------------------------------------------------------+ -| ``-db database`` | Database alias as defied in ``gor.sql.credentials``. | +| ``-db database`` | Database alias as defied in ``gor.db.credentials``. | +----------------------+---------------------------------------------------------------------------------------------------+ diff --git a/gortools/src/test/java/gorsat/UTestSQLInputSource.java b/gortools/src/test/java/gorsat/UTestSQLInputSource.java index 649ec791..46b1eb31 100644 --- a/gortools/src/test/java/gorsat/UTestSQLInputSource.java +++ b/gortools/src/test/java/gorsat/UTestSQLInputSource.java @@ -58,9 +58,9 @@ public static void initDb() throws IOException, ClassNotFoundException, SQLExcep List credFileLines = Files.readAllLines(Path.of(rdaPaths[2])); credFileLines.add(Files.readAllLines(Path.of(avasPaths[2])).get(1)); - File credFile = FileTestUtils.createTempFile(new File(rdaPaths[0]), "gor.sql.credentials", + File credFile = FileTestUtils.createTempFile(new File(rdaPaths[0]), "gor.db.credentials", credFileLines.stream().collect(Collectors.joining("\n"))); - System.setProperty("gor.sql.credentials", credFile.getAbsolutePath()); + System.setProperty("gor.db.credentials", credFile.getAbsolutePath()); DbConnection.initInConsoleApp(); } diff --git a/gortools/src/test/scala/gorsat/UTestInputSourceParsing.scala b/gortools/src/test/scala/gorsat/UTestInputSourceParsing.scala index db304ac8..31a20597 100644 --- a/gortools/src/test/scala/gorsat/UTestInputSourceParsing.scala +++ b/gortools/src/test/scala/gorsat/UTestInputSourceParsing.scala @@ -119,7 +119,7 @@ class UTestInputSourceParsing extends AnyFunSuite with BeforeAndAfter with Mocki Class.forName("org.apache.derby.jdbc.EmbeddedDriver") val paths = createTestDataBase_Derby - System.setProperty("gor.sql.credentials", paths(2)) + System.setProperty("gor.db.credentials", paths(2)) DbConnection.initInConsoleApp() var tempDirectory = FileTestUtils.createTempDirectory(this.getClass.getName) diff --git a/model/src/main/java/org/gorpipe/gor/model/DbConnection.java b/model/src/main/java/org/gorpipe/gor/model/DbConnection.java index aa7c8d73..eed8ea8a 100644 --- a/model/src/main/java/org/gorpipe/gor/model/DbConnection.java +++ b/model/src/main/java/org/gorpipe/gor/model/DbConnection.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.lang.reflect.UndeclaredThrowableException; import java.sql.*; +import java.util.List; /** * DbConnection abstract access to database source that are configured on installation time. @@ -43,24 +44,30 @@ * The static part creates a cache of DbConnection objects that can be used to access all databases defined in the * database configuration files. This part is located here for backward compatibility reasons. *

    - * We have two different configuration files: + * We have one configuration file: * - *

  • gor.db.credentials - contains the system databases, which are used by the system internally (e.g. session management), - * and by operation where we have strict access controls (db://, //db:). These credentials - * typically would grant full access to the database. - *
  • gor.sql.credentials - contains the user databases, which are used by the user available commands/sources (SQL, - * GORSQL, NORSQL, sql://). These credentials typically would grant limited/read-only access to - * the database. + *
  • gor.db.credentials - contains all the databases gor can reach. It feeds both the system connections, + * used internally (e.g. session management) and by operations with strict access + * controls (db://, //db:), and the user connections behind the user available + * commands/sources (SQL, GORSQL, NORSQL, sql://). *


    - * The format for these files is: + * In a server, the two caches are fed from separate sources. {@link #userConnections} comes from the + * gor.db.credentials file, while {@link #systemConnections} comes from credentials the host passes in + * as {@link DbCredentials} — see {@link #initInServer(java.util.List)}. System credentials typically + * rotate, and so cannot be baked into a file without going stale; gor does not care where the host got + * them and never reads them from the environment itself. + *


    + * In a console app there is no host to supply credentials, so both caches load from the file. + *


    + * The format for this file is: *

      *    name\tdriver\turl\tuser\tpwd
      *    rda\torg.postgresql.Driver\tjdbc:postgresql://myurl.com:5432/csa\trda\tmypass
      *    ...
      * 
    *

    - * The location of the files defaults to the config directory but can be specified by the system properties - * gor.db.credentials and gor.sql.credentials. + * The location of the file defaults to the config directory but can be specified by the system property + * gor.db.credentials. *


    * TODO: This class needs some refactoring to handle the different databases more gracefully. We should use some kind of * plugin mechanism (or Guice) instead of if/else statements. @@ -244,10 +251,7 @@ public static void initInConsoleApp() throws ClassNotFoundException, IOException File homeDbCredFile = new File(System.getProperty("user.home"), "gor.db.credentials"); final String dbCredpath = homeDbCredFile.exists() ? homeDbCredFile.getCanonicalPath() : System.getProperty("gor.db.credentials"); systemConnections.initializeDbSources(dbCredpath); - - File homeSqlCredFile = new File(System.getProperty("user.home"), "gor.sql.credentials"); - final String sqlCredpath = homeSqlCredFile.exists() ? homeSqlCredFile.getCanonicalPath() : System.getProperty("gor.sql.credentials"); - userConnections.initializeDbSources(sqlCredpath); + userConnections.initializeDbSources(dbCredpath); setPasswordCallback(s -> { final Console console = System.console(); @@ -261,16 +265,39 @@ public static void initInConsoleApp() throws ClassNotFoundException, IOException * @throws ClassNotFoundException * @throws IOException */ - @SuppressWarnings("unused") // Called from GorQueryTask in gor-services + @SuppressWarnings("unused") // Called from gor-services public static void initInServer() throws ClassNotFoundException, IOException { - final String dbCredpath = System.getProperty("gor.db.credentials"); - if (dbCredpath != null) { - systemConnections.initializeDbSources(dbCredpath); - } + initInServer(List.of()); + } + + /** + * Initialize DbSource to be used in a server, with the system credentials supplied by the host + * application. + * + * The two caches are fed from separate sources: + * + *

  • {@link #systemConnections}, used internally and by the access-controlled operations + * (db://, //db:), comes from the supplied credentials. These typically rotate, so a host + * holding them — in its own configuration, a secret manager, or environment variables it + * owns the naming of — passes them here rather than gor reading them itself. + *
  • {@link #userConnections}, behind the user-available commands and sources (SQL, GORSQL, + * NORSQL, sql://), comes from the gor.db.credentials file. + * + * The sources are kept apart deliberately: the file cannot hold credentials that rotate without + * going stale, and the rotating system credentials are not the ones user queries should reach. + * + * @param systemCredentials Credentials for {@link #systemConnections}. May be empty, which leaves + * that cache empty. + * @throws ClassNotFoundException + * @throws IOException + */ + @SuppressWarnings("unused") // Called from GorQueryTask in gor-services + public static void initInServer(List systemCredentials) throws ClassNotFoundException, IOException { + systemConnections.initializeDbSources(systemCredentials); - final String sqlCredpath = System.getProperty("gor.sql.credentials"); - if (sqlCredpath != null) { - userConnections.initializeDbSources(sqlCredpath); + final String userCredpath = System.getProperty("gor.db.credentials"); + if (userCredpath != null) { + userConnections.initializeDbSources(userCredpath); } } diff --git a/model/src/main/java/org/gorpipe/gor/model/DbConnectionCache.java b/model/src/main/java/org/gorpipe/gor/model/DbConnectionCache.java index f70db082..440652d5 100644 --- a/model/src/main/java/org/gorpipe/gor/model/DbConnectionCache.java +++ b/model/src/main/java/org/gorpipe/gor/model/DbConnectionCache.java @@ -12,6 +12,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -45,35 +46,55 @@ public DbConnection lookup(String source) { } /** - * Read Database sources from configuration file + * Read database sources from the configuration file, replacing any sources already installed. * * @param credpath The path to the configuration file - * @throws ClassNotFoundException - * @throws IOException + * @throws IOException if the credentials file is configured but missing */ @SuppressWarnings("WeakerAccess") // Used from gor-services public void initializeDbSources(String credpath) throws IOException { clearDbSources(); - if (credpath != null && credpath.trim().length() > 0) { - final Path path = Paths.get(credpath); - if (Files.notExists(path)) { - throw new FileNotFoundException("Specified db credentials file (" + credpath + ") is not found"); - } + installAllFromParts(readFileForDbSourceInstallation(credpath)); + } - final List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + /** + * Install database sources from credentials supplied by the host application, replacing any + * sources already installed. + * + * This is the counterpart to {@link #initializeDbSources(String)} for credentials that rotate and + * so cannot be baked into a file. The host may source them from its own configuration, a secret + * manager, or environment variables it owns the naming of — gor does not read them itself. + * + * @param credentials The credentials to install. May be empty, which leaves the cache empty. + */ + public void initializeDbSources(List credentials) { + clearDbSources(); + installAllFromParts(toPartsForInstallation(credentials)); + } + + private List readFileForDbSourceInstallation(String credpath) throws IOException { + if (credpath == null || credpath.trim().length() == 0) { + log.info("No db credential path specified"); + return Collections.emptyList(); + } - List partsList = parseLinesForDbSourceInstallation(credpath, lines); + final Path path = Paths.get(credpath); + if (Files.notExists(path)) { + throw new FileNotFoundException("Specified db credentials file (" + credpath + ") is not found"); + } - for (String[] parts : partsList) { - try { - installDbSourceFromParts(parts); - } catch (ClassNotFoundException e) { - log.error("Failed to load driver class {} for db source {}. Please ensure the driver is in the classpath.", - parts[1], parts[0], e); - } + final List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + return parseLinesForDbSourceInstallation(credpath, lines); + } + + private void installAllFromParts(List partsList) { + for (String[] parts : partsList) { + try { + installDbSourceFromParts(parts); + } catch (ClassNotFoundException e) { + log.error("Failed to load driver class {} for db source {}. Please ensure the driver is in the classpath.", + parts[1], parts[0], e); } - } else { - log.info("No db credential path specified"); } } @@ -111,6 +132,83 @@ public static List parseLinesForDbSourceInstallation(String credpath, return partsList; } + /** + * Convert caller-supplied credentials into the {name, driver, url, user[, pwd]} shape that + * parseLinesForDbSourceInstallation produces, so both paths share the install code. + * + * Incomplete credentials are skipped with a warning rather than failing initialization. Blank + * values count as unset, including the password: a secret manager or template that renders an + * empty value is expressing a missing value, not a real empty password. + * + * Never logs credential values, only the source name and which field was missing. + * + * @param credentials the credentials to convert, may be null + * @return one entry per usable credential + */ + static List toPartsForInstallation(List credentials) { + List partsList = new ArrayList<>(); + if (credentials == null) { + return partsList; + } + + for (DbCredentials cred : credentials) { + if (cred == null) { + continue; + } + + String name = trimToNull(cred.name()); + String url = trimToNull(cred.url()); + String user = trimToNull(cred.user()); + String pwd = trimToNull(cred.pwd()); + + if (name == null || url == null || user == null) { + log.warn("Incomplete db source credentials for source {}: {} is not set. Ignoring it.", + name == null ? "" : name, + name == null ? "name" : (url == null ? "url" : "user")); + continue; + } + + String driver = trimToNull(cred.driver()); + if (driver == null) { + driver = driverClassForUrl(url); + } + if (driver == null) { + log.warn("Could not derive a jdbc driver for db source {} from its url, and no driver was given. Ignoring it.", + name); + continue; + } + + if (pwd == null) { + partsList.add(new String[]{name, driver, url, user}); + } else { + partsList.add(new String[]{name, driver, url, user, pwd}); + } + } + return partsList; + } + + /** + * @param url a jdbc url + * @return the matching driver class name, or null if the prefix is not recognized + */ + static String driverClassForUrl(String url) { + if (url.startsWith("jdbc:postgresql:")) { + return "org.postgresql.Driver"; + } + if (url.startsWith("jdbc:oracle:")) { + return "oracle.jdbc.driver.OracleDriver"; + } + return null; + } + + private static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private void installDbSourceFromParts(String[] parts) throws ClassNotFoundException { Class.forName(parts[1]); // Just load the driver once and for all final DbConnection source = new DbConnection(parts[0], parts[2], parts[3], parts.length > 4 ? parts[4] : null); diff --git a/model/src/main/java/org/gorpipe/gor/model/DbCredentials.java b/model/src/main/java/org/gorpipe/gor/model/DbCredentials.java new file mode 100644 index 00000000..51a986c6 --- /dev/null +++ b/model/src/main/java/org/gorpipe/gor/model/DbCredentials.java @@ -0,0 +1,51 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package org.gorpipe.gor.model; + +/** + * Credentials for a single database source, supplied programmatically rather than through the + * credentials file. + * + * This exists so that a host application can source credentials however it likes — from its own + * configuration, a secret manager, or environment variables it owns the naming of — and hand them to + * {@link DbConnectionCache#initializeDbSources(String, java.util.List)} without gor needing to know + * where they came from. + * + * Credentials passed this way are installed before the credentials file is read, so a file row with + * the same name takes precedence. + * + * @param name the source name, e.g. "rda". Required. + * @param url the jdbc url. Required. + * @param user the database user. Required. + * @param pwd the password. May be null. + * @param driver the jdbc driver class. May be null, in which case it is derived from the url prefix. + */ +public record DbCredentials(String name, String url, String user, String pwd, String driver) { + + /** + * Credentials with the driver derived from the url prefix. + */ + public DbCredentials(String name, String url, String user, String pwd) { + this(name, url, user, pwd, null); + } +} diff --git a/model/src/test/java/org/gorpipe/gor/model/UTestDbSource.java b/model/src/test/java/org/gorpipe/gor/model/UTestDbSource.java index bc199e6d..42edae10 100644 --- a/model/src/test/java/org/gorpipe/gor/model/UTestDbSource.java +++ b/model/src/test/java/org/gorpipe/gor/model/UTestDbSource.java @@ -23,8 +23,14 @@ package org.gorpipe.gor.model; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import java.io.File; +import java.io.FileNotFoundException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -71,4 +77,172 @@ public void parseLinesForDbSourceInstallationWithRealTabsAndNewlinesAndInvalidLi Assert.assertEquals(1, partsList.size()); Assert.assertEquals(5, partsList.get(0).length); } -} \ No newline at end of file + + private static List rdaCreds(String url, String username, String password) { + return List.of(new DbCredentials("rda", url, username, password)); + } + + private static List rdaCreds(String url, String username, String password, String driver) { + return List.of(new DbCredentials("rda", url, username, password, driver)); + } + + @Test + public void credentialsBuildPartsInFileParserShape() { + List creds = rdaCreds("jdbc:postgresql://db:5432/csa", "gregor_reader", "secret"); + List partsList = DbConnectionCache.toPartsForInstallation(creds); + Assert.assertEquals(1, partsList.size()); + String[] parts = partsList.get(0); + Assert.assertEquals(5, parts.length); + Assert.assertEquals("rda", parts[0]); + Assert.assertEquals("org.postgresql.Driver", parts[1]); + Assert.assertEquals("jdbc:postgresql://db:5432/csa", parts[2]); + Assert.assertEquals("gregor_reader", parts[3]); + Assert.assertEquals("secret", parts[4]); + } + + @Test + public void credentialsOmitPasswordFieldWhenPasswordUnset() { + List creds = rdaCreds("jdbc:postgresql://db:5432/csa", "gregor_reader", null); + List partsList = DbConnectionCache.toPartsForInstallation(creds); + Assert.assertEquals(1, partsList.size()); + Assert.assertEquals(4, partsList.get(0).length); + } + + @Test + public void credentialsTreatBlankPasswordAsUnset() { + List creds = rdaCreds("jdbc:postgresql://db:5432/csa", "gregor_reader", " "); + List partsList = DbConnectionCache.toPartsForInstallation(creds); + Assert.assertEquals(1, partsList.size()); + Assert.assertEquals("blank password should be omitted, not installed as an empty password", + 4, partsList.get(0).length); + } + + @Test + public void noCredentialsProducesNoParts() { + Assert.assertTrue(DbConnectionCache.toPartsForInstallation(List.of()).isEmpty()); + } + + @Test + public void credentialsSkippedWhenUsernameMissing() { + List creds = rdaCreds("jdbc:postgresql://db:5432/csa", null, "secret"); + Assert.assertTrue(DbConnectionCache.toPartsForInstallation(creds).isEmpty()); + } + + @Test + public void credentialsSkippedWhenUrlMissing() { + List creds = rdaCreds(null, "gregor_reader", "secret"); + Assert.assertTrue(DbConnectionCache.toPartsForInstallation(creds).isEmpty()); + } + + @Test + public void credentialsTreatBlankValuesAsUnset() { + List creds = rdaCreds("jdbc:postgresql://db:5432/csa", " ", "secret"); + Assert.assertTrue(DbConnectionCache.toPartsForInstallation(creds).isEmpty()); + } + + @Test + public void credentialsDeriveOracleDriverFromUrlPrefix() { + List creds = rdaCreds("jdbc:oracle:thin:@db:1521:XE", "gregor_reader", "secret"); + List partsList = DbConnectionCache.toPartsForInstallation(creds); + Assert.assertEquals(1, partsList.size()); + Assert.assertEquals("oracle.jdbc.driver.OracleDriver", partsList.get(0)[1]); + } + + @Test + public void credentialsHonourExplicitDriverOverride() { + List creds = rdaCreds("jdbc:whatever://db/x", "gregor_reader", "secret", "com.example.Driver"); + List partsList = DbConnectionCache.toPartsForInstallation(creds); + Assert.assertEquals(1, partsList.size()); + Assert.assertEquals("com.example.Driver", partsList.get(0)[1]); + } + + @Test + public void credentialsSkippedWhenDriverCannotBeDerived() { + List creds = rdaCreds("jdbc:whatever://db/x", "gregor_reader", "secret"); + Assert.assertTrue(DbConnectionCache.toPartsForInstallation(creds).isEmpty()); + } + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private String writeCredentialsFile(String... lines) throws Exception { + File file = tempFolder.newFile("gor.db.credentials"); + Files.write(file.toPath(), String.join("\n", lines).getBytes(StandardCharsets.UTF_8)); + return file.getAbsolutePath(); + } + + @Test + public void suppliedCredentialsOnlyInstallRdaSource() { + DbConnectionCache cache = new DbConnectionCache(); + cache.initializeDbSources(rdaCreds("jdbc:postgresql://db:5432/csa", "gregor_reader", "secret")); + + DbConnection rda = cache.lookup("rda"); + Assert.assertNotNull("rda source should be installed from the supplied credentials", rda); + Assert.assertEquals("jdbc:postgresql://db:5432/csa", rda.url); + Assert.assertEquals("gregor_reader", rda.user); + Assert.assertEquals("secret", rda.pwd); + } + + @Test + public void blankSuppliedPasswordInstallsSourceWithNullPassword() { + DbConnectionCache cache = new DbConnectionCache(); + cache.initializeDbSources(rdaCreds("jdbc:postgresql://db:5432/csa", "gregor_reader", " ")); + + DbConnection rda = cache.lookup("rda"); + Assert.assertNotNull("source should still install, only the password is unset", rda); + Assert.assertNull("whitespace password must not reach the connection as a real password", rda.pwd); + } + + @Test + public void partialCredentialsSkippedWithoutThrowing() { + DbConnectionCache cache = new DbConnectionCache(); + cache.initializeDbSources(rdaCreds("jdbc:postgresql://db:5432/csa", null, "secret")); + + Assert.assertNull(cache.lookup("rda")); + } + + @Test + public void suppliedCredentialsReplaceAnythingAlreadyInstalled() throws Exception { + String credpath = writeCredentialsFile( + "name\tdriver\turl\tuser\tpwd", + "aux\torg.postgresql.Driver\tjdbc:postgresql://auxhost:5432/aux\tauxuser\tauxpwd"); + + DbConnectionCache cache = new DbConnectionCache(); + cache.initializeDbSources(credpath); + Assert.assertNotNull(cache.lookup("aux")); + + // The two initializers are alternatives, not additive - each clears the cache first, which is + // what keeps the system and user caches fed from exactly one source each. + cache.initializeDbSources(rdaCreds("jdbc:postgresql://db:5432/csa", "gregor_reader", "secret")); + Assert.assertNotNull(cache.lookup("rda")); + Assert.assertNull("file sources should not survive a credentials load", cache.lookup("aux")); + } + + @Test + public void fileInstallsEveryRow() throws Exception { + String credpath = writeCredentialsFile( + "name\tdriver\turl\tuser\tpwd", + "rda\torg.postgresql.Driver\tjdbc:postgresql://filehost:5432/csa\tfileuser\tfilepwd", + "aux\torg.postgresql.Driver\tjdbc:postgresql://auxhost:5432/aux\tauxuser\tauxpwd"); + + DbConnectionCache cache = new DbConnectionCache(); + cache.initializeDbSources(credpath); + + Assert.assertEquals("fileuser", cache.lookup("rda").user); + Assert.assertEquals("auxuser", cache.lookup("aux").user); + } + + @Test(expected = FileNotFoundException.class) + public void missingFileThrows() throws Exception { + String missing = new File(tempFolder.getRoot(), "does-not-exist.credentials").getAbsolutePath(); + new DbConnectionCache().initializeDbSources(missing); + } + + @Test + public void noCredentialPathLeavesCacheEmpty() throws Exception { + DbConnectionCache cache = new DbConnectionCache(); + cache.initializeDbSources((String) null); + + Assert.assertNull(cache.lookup("rda")); + } +} diff --git a/model/src/test/java/org/gorpipe/gor/model/UTestSqlSource.java b/model/src/test/java/org/gorpipe/gor/model/UTestSqlSource.java index 032dd0c8..392c3850 100644 --- a/model/src/test/java/org/gorpipe/gor/model/UTestSqlSource.java +++ b/model/src/test/java/org/gorpipe/gor/model/UTestSqlSource.java @@ -26,7 +26,6 @@ public class UTestSqlSource { public static void setup() throws IOException, ClassNotFoundException, SQLException { paths = DatabaseHelper.createRdaDatabase(); System.setProperty("gor.db.credentials", paths[2]); - System.setProperty("gor.sql.credentials", paths[2]); DbConnection.initInConsoleApp(); }