Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,48 @@ Accepted formats:
- `<user>/<password>@//<host>[:<port>]/<service>`
- `<user>/<password>@<host>:<port>:<SID>`
- `<user>/<password>@<TNSName>`
- `/@<TNSName>` - credentials are taken from an Oracle Wallet (Secure External Password Store), see [Oracle Wallet](#oracle-wallet-secure-external-password-store)

To connect using TNS, you need to have the ORACLE_HOME environment variable set.
The file tnsnames.ora must exist in path %ORACLE_HOME%/network/admin
The file tnsnames.ora must contain valid TNS entries.
To connect using a TNS name, the file `tnsnames.ora` with a valid entry for that name must be found.
The directory holding `tnsnames.ora` (and `ojdbc.properties`, if used) is taken from the first of these that is set:

1. `TNS_ADMIN` parameter in the connect string, e.g. `app/pass@MYDATABASE?TNS_ADMIN=/path/to/network/admin`
2. Java system property `oracle.net.tns_admin`, e.g. `export JAVA_OPTS="-Doracle.net.tns_admin=/path/to/network/admin"`
3. `TNS_ADMIN` environment variable
4. `$ORACLE_HOME/network/admin`, when the `ORACLE_HOME` environment variable is set

Options 1-3 are handled by the Oracle JDBC driver. Option 4 is a fallback provided by utPLSQL-cli, used only when none of the others is set.

In case you use a username containing `/` or a password containing `@` you should encapsulate it with double quotes `"`:
```
utplsql run "my/Username"/"myP@ssword"@connectstring
```

#### Oracle Wallet (Secure External Password Store)

To avoid passing the password on the command line, store the credentials in an Oracle Wallet and connect with `/@<TNSName>`:
```
utplsql run /@MYDATABASE
```

Setup example:
```
# create an auto-login wallet with credentials for TNS alias MYDATABASE
orapki wallet create -wallet $HOME/oracle/wallet -auto_login_local
mkstore -wrl $HOME/oracle/wallet -createCredential MYDATABASE someusername

# point the JDBC driver to the wallet
echo "oracle.net.wallet_location=(SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY=$HOME/oracle/wallet)))" \
> $HOME/oracle/network/admin/ojdbc.properties

# tnsnames.ora with the MYDATABASE entry must be in the same directory as ojdbc.properties
export TNS_ADMIN=$HOME/oracle/network/admin
```

`ojdbc.properties` is read from the same directory as `tnsnames.ora`, so any of the options listed under [ConnectionURL](#connectionurl) can be used instead of `TNS_ADMIN`, for example `utplsql run "/@MYDATABASE?TNS_ADMIN=/path/to/network/admin"`.

The TNS alias used in the connect string must match the alias of the credential stored in the wallet.

### run
`utplsql run <ConnectionURL> [<options>]`

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>

<utplsql-java-api.version>3.2.4</utplsql-java-api.version>
<utplsql-java-api.version>3.2.5-SNAPSHOT</utplsql-java-api.version>

<junit.jupiter.version>5.12.2</junit.jupiter.version>
<picocli.version>4.7.7</picocli.version>
Expand Down
23 changes: 20 additions & 3 deletions src/main/java/org/utplsql/cli/ConnectionConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@

public class ConnectionConfig {

/**
* Either {@code <user>/<password>@<connect>} or {@code /@<connect>}.
*/
private static final Pattern CONNECT_STRING_PATTERN =
Pattern.compile("^(?:(\".+\"|[^/]+)/(\".+\"|[^@]+)|/)@(.*)$");

Check warning on line 12 in src/main/java/org/utplsql/cli/ConnectionConfig.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=utPLSQL_utPLSQL-cli&issues=AaDimV4vqHqnOTNnu2Az&open=AaDimV4vqHqnOTNnu2Az&pullRequest=230

private final String user;
private final String password;
private final String connect;

public ConnectionConfig(String connectString) {
Matcher m = Pattern.compile("^(\".+\"|[^/]+)/(\".+\"|[^@]+)@(.*)$").matcher(connectString);
Matcher m = CONNECT_STRING_PATTERN.matcher(connectString);
if (m.find()) {
user = stripEnclosingQuotes(m.group(1));
password = stripEnclosingQuotes(m.group(2));
user = m.group(1) == null ? null : stripEnclosingQuotes(m.group(1));
password = m.group(2) == null ? null : stripEnclosingQuotes(m.group(2));
connect = m.group(3);
} else {
throw new IllegalArgumentException("Not a valid connectString: '" + connectString + "'");
Expand Down Expand Up @@ -42,7 +48,18 @@
return password;
}

/**
* @return true when no user/password was given (connect string {@code /@<connect>}),
* meaning credentials are provided externally, e.g. by an Oracle Wallet
*/
public boolean isExternalAuthentication() {
return user == null;
}

public String getConnectString() {
if (isExternalAuthentication()) {
return "/@" + connect;
}
return user + "/" + password + "@" + connect;
}

Expand Down
28 changes: 24 additions & 4 deletions src/main/java/org/utplsql/cli/DataSourceProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,32 @@
*/
public class DataSourceProvider {

private static final String TNS_ADMIN_PROPERTY = "oracle.net.tns_admin";

static {
String oracleHome = System.getenv("ORACLE_HOME");
if (oracleHome != null && System.getProperty("oracle.net.tns_admin") == null) {
System.setProperty("oracle.net.tns_admin",
String.join(File.separator, oracleHome, "NETWORK", "ADMIN"));
String tnsAdminFallback = getTnsAdminFallback(
System.getProperty(TNS_ADMIN_PROPERTY), System.getenv("TNS_ADMIN"), System.getenv("ORACLE_HOME"));
if (tnsAdminFallback != null) {
System.setProperty(TNS_ADMIN_PROPERTY, tnsAdminFallback);
}
}

/**
* The JDBC driver resolves tnsnames.ora / ojdbc.properties from the {@value TNS_ADMIN_PROPERTY} property
* or the TNS_ADMIN environment variable on its own, but it doesn't look into ORACLE_HOME.
* The property takes precedence over the environment variable, so it must not be set when TNS_ADMIN is.
*
* @return ORACLE_HOME/network/admin (lowercase, as in Oracle installations; paths are case-sensitive on Linux) when neither {@value TNS_ADMIN_PROPERTY} nor TNS_ADMIN is set, otherwise null
*/
static String getTnsAdminFallback(String tnsAdminProperty, String tnsAdminEnv, String oracleHome) {
if (isEmpty(tnsAdminProperty) && isEmpty(tnsAdminEnv) && !isEmpty(oracleHome)) {
return String.join(File.separator, oracleHome, "network", "admin");
}
return null;
}

private static boolean isEmpty(String value) {
return value == null || value.isEmpty();
}

public static DataSource getDataSource(String connectString, int maxConnections) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,19 @@
List<String> errors = new ArrayList<>();
Throwable lastException = null;

ds.setUser(config.getUser());
ds.setPassword(config.getPassword());
// With external authentication (Oracle Wallet) the driver looks up the credentials itself
if (!config.isExternalAuthentication()) {
ds.setUser(config.getUser());
ds.setPassword(config.getPassword());
}

for (ConnectStringPossibility possibility : possibilities) {
logger.debug("Try connecting {}", possibility.getMaskedConnectString(config));

Check warning on line 59 in src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Invoke method(s) only conditionally.

See more on https://sonarcloud.io/project/issues?id=utPLSQL_utPLSQL-cli&issues=AaDimV7ZqHqnOTNnu2A0&open=AaDimV7ZqHqnOTNnu2A0&pullRequest=230
ds.setURL(possibility.getConnectString(config));
try (Connection ignored = ds.getConnection()) {
logger.info("Use connection string {}", possibility.getMaskedConnectString(config));

Check warning on line 62 in src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Invoke method(s) only conditionally.

See more on https://sonarcloud.io/project/issues?id=utPLSQL_utPLSQL-cli&issues=AaDimV7ZqHqnOTNnu2A1&open=AaDimV7ZqHqnOTNnu2A1&pullRequest=230
return;
} catch (Error | Exception e) {

Check warning on line 64 in src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Catch Exception instead of Error.

See more on https://sonarcloud.io/project/issues?id=utPLSQL_utPLSQL-cli&issues=AaDimV7ZqHqnOTNnu2A2&open=AaDimV7ZqHqnOTNnu2A2&pullRequest=230
errors.add(possibility.getMaskedConnectString(config) + ": " + e.getMessage());
lastException = e;
}
Expand All @@ -84,15 +87,15 @@
sqlCommands.add(String.format("ALTER SESSION SET NLS_TERRITORY='%s'", matcher.group(2)));
}

if (sqlCommands.size() > 0) {
if (!sqlCommands.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("BEGIN\n");
for (String command : sqlCommands) {
sb.append(String.format("EXECUTE IMMEDIATE q'[%s]';\n", command));
}
sb.append("END;");

logger.debug("NLS settings: {}", sb.toString());
logger.debug("NLS settings: {}", sb);
ds.setConnectionInitSql(sb.toString());
}
}
Expand All @@ -107,7 +110,7 @@

@Override
public String getMaskedConnectString(ConnectionConfig config) {
return "jdbc:oracle:oci8:****/****@" + config.getConnect();
return "jdbc:oracle:oci8:" + maskedCredentials(config) + "@" + config.getConnect();
}
}

Expand All @@ -119,7 +122,11 @@

@Override
public String getMaskedConnectString(ConnectionConfig config) {
return "jdbc:oracle:thin:****/****@" + config.getConnect();
return "jdbc:oracle:thin:" + maskedCredentials(config) + "@" + config.getConnect();
}
}

private static String maskedCredentials(ConnectionConfig config) {
return config.isExternalAuthentication() ? "/" : "****/****";
}
}
52 changes: 52 additions & 0 deletions src/test/java/org/utplsql/cli/ConnectionConfigTest.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.utplsql.cli;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.*;

public class ConnectionConfigTest {

@Test
void parse() {

Check warning on line 12 in src/test/java/org/utplsql/cli/ConnectionConfigTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace these 3 tests with a single Parameterized one.

See more on https://sonarcloud.io/project/issues?id=utPLSQL_utPLSQL-cli&issues=AaDimV9-qHqnOTNnu2A4&open=AaDimV9-qHqnOTNnu2A4&pullRequest=230
ConnectionConfig info = new ConnectionConfig("test/pw@my.local.host/service");

assertEquals("test", info.getUser());
Expand Down Expand Up @@ -54,4 +56,54 @@
assertEquals("my.local.host/service", info.getConnect());
assertFalse(info.isSysDba());
}

@Test
void parseCredentialsIsNotExternalAuthentication() {
ConnectionConfig info = new ConnectionConfig("test/pw@MY_TNS_ALIAS");

assertFalse(info.isExternalAuthentication());
assertEquals("test/pw@MY_TNS_ALIAS", info.getConnectString());
}

@Test
void parseExternalAuthentication() {
ConnectionConfig info = new ConnectionConfig("/@MY_TNS_ALIAS");

assertNull(info.getUser());
assertNull(info.getPassword());
assertEquals("MY_TNS_ALIAS", info.getConnect());
assertTrue(info.isExternalAuthentication());
assertFalse(info.isSysDba());
assertEquals("/@MY_TNS_ALIAS", info.getConnectString());
}

@Test
void parseExternalAuthenticationWithTnsAdminInUrl() {
ConnectionConfig info = new ConnectionConfig("/@MY_TNS_ALIAS?TNS_ADMIN=/home/me/oracle/network/admin");

assertNull(info.getUser());
assertNull(info.getPassword());
assertEquals("MY_TNS_ALIAS?TNS_ADMIN=/home/me/oracle/network/admin", info.getConnect());
assertTrue(info.isExternalAuthentication());
}

@Test
void parseExternalAuthenticationWithEzConnect() {
ConnectionConfig info = new ConnectionConfig("/@//my.local.host:1521/service");

assertEquals("//my.local.host:1521/service", info.getConnect());
assertTrue(info.isExternalAuthentication());
}

@ParameterizedTest
@ValueSource(strings = {
"/pw@MY_TNS_ALIAS", // password without user
"test/@MY_TNS_ALIAS", // user without password
"@MY_TNS_ALIAS",
"test@MY_TNS_ALIAS",
"MY_TNS_ALIAS"
})
void rejectInvalidConnectString(String connectString) {
assertThrows(IllegalArgumentException.class, () -> new ConnectionConfig(connectString));
}
}
36 changes: 36 additions & 0 deletions src/test/java/org/utplsql/cli/DataSourceProviderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.utplsql.cli;

import org.junit.jupiter.api.Test;

import java.io.File;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class DataSourceProviderTest {

private static final String ORACLE_HOME = "/opt/oracle";

@Test
void tnsAdminFallsBackToOracleHome() {
assertEquals(String.join(File.separator, ORACLE_HOME, "network", "admin"),
DataSourceProvider.getTnsAdminFallback(null, null, ORACLE_HOME));
}

@Test
void tnsAdminEnvVariableIsLeftToJdbcDriver() {
// Setting the property would override the TNS_ADMIN environment variable in the JDBC driver
assertNull(DataSourceProvider.getTnsAdminFallback(null, "/my/tns_admin", ORACLE_HOME));
}

@Test
void tnsAdminPropertyIsNotOverridden() {
assertNull(DataSourceProvider.getTnsAdminFallback("/my/tns_admin", null, ORACLE_HOME));
}

@Test
void noFallbackWithoutOracleHome() {
assertNull(DataSourceProvider.getTnsAdminFallback(null, null, null));
assertNull(DataSourceProvider.getTnsAdminFallback("", "", ""));
}
}
Loading
Loading