Skip to content

Commit 8c83e6a

Browse files
authored
Merge pull request #230 from utPLSQL/feature/add_support_for_wallet
Allow `/@TNS` connections using Oracle Wallet (Secure External Password Store)
2 parents 7bd004c + 96f9f9f commit 8c83e6a

8 files changed

Lines changed: 353 additions & 17 deletions

File tree

‎README.md‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,48 @@ Accepted formats:
9898
- `<user>/<password>@//<host>[:<port>]/<service>`
9999
- `<user>/<password>@<host>:<port>:<SID>`
100100
- `<user>/<password>@<TNSName>`
101+
- `/@<TNSName>` - credentials are taken from an Oracle Wallet (Secure External Password Store), see [Oracle Wallet](#oracle-wallet-secure-external-password-store)
101102

102-
To connect using TNS, you need to have the ORACLE_HOME environment variable set.
103-
The file tnsnames.ora must exist in path %ORACLE_HOME%/network/admin
104-
The file tnsnames.ora must contain valid TNS entries.
103+
To connect using a TNS name, the file `tnsnames.ora` with a valid entry for that name must be found.
104+
The directory holding `tnsnames.ora` (and `ojdbc.properties`, if used) is taken from the first of these that is set:
105+
106+
1. `TNS_ADMIN` parameter in the connect string, e.g. `app/pass@MYDATABASE?TNS_ADMIN=/path/to/network/admin`
107+
2. Java system property `oracle.net.tns_admin`, e.g. `export JAVA_OPTS="-Doracle.net.tns_admin=/path/to/network/admin"`
108+
3. `TNS_ADMIN` environment variable
109+
4. `$ORACLE_HOME/network/admin`, when the `ORACLE_HOME` environment variable is set
110+
111+
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.
105112

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

118+
#### Oracle Wallet (Secure External Password Store)
119+
120+
To avoid passing the password on the command line, store the credentials in an Oracle Wallet and connect with `/@<TNSName>`:
121+
```
122+
utplsql run /@MYDATABASE
123+
```
124+
125+
Setup example:
126+
```
127+
# create an auto-login wallet with credentials for TNS alias MYDATABASE
128+
orapki wallet create -wallet $HOME/oracle/wallet -auto_login_local
129+
mkstore -wrl $HOME/oracle/wallet -createCredential MYDATABASE someusername
130+
131+
# point the JDBC driver to the wallet
132+
echo "oracle.net.wallet_location=(SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY=$HOME/oracle/wallet)))" \
133+
> $HOME/oracle/network/admin/ojdbc.properties
134+
135+
# tnsnames.ora with the MYDATABASE entry must be in the same directory as ojdbc.properties
136+
export TNS_ADMIN=$HOME/oracle/network/admin
137+
```
138+
139+
`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"`.
140+
141+
The TNS alias used in the connect string must match the alias of the credential stored in the wallet.
142+
111143
### run
112144
`utplsql run <ConnectionURL> [<options>]`
113145

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1515
<maven.compiler.release>17</maven.compiler.release>
1616

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

1919
<junit.jupiter.version>5.12.2</junit.jupiter.version>
2020
<picocli.version>4.7.7</picocli.version>

‎src/main/java/org/utplsql/cli/ConnectionConfig.java‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55

66
public class ConnectionConfig {
77

8+
/**
9+
* Either {@code <user>/<password>@<connect>} or {@code /@<connect>}.
10+
*/
11+
private static final Pattern CONNECT_STRING_PATTERN =
12+
Pattern.compile("^(?:(\".+\"|[^/]+)/(\".+\"|[^@]+)|/)@(.*)$");
13+
814
private final String user;
915
private final String password;
1016
private final String connect;
1117

1218
public ConnectionConfig(String connectString) {
13-
Matcher m = Pattern.compile("^(\".+\"|[^/]+)/(\".+\"|[^@]+)@(.*)$").matcher(connectString);
19+
Matcher m = CONNECT_STRING_PATTERN.matcher(connectString);
1420
if (m.find()) {
15-
user = stripEnclosingQuotes(m.group(1));
16-
password = stripEnclosingQuotes(m.group(2));
21+
user = m.group(1) == null ? null : stripEnclosingQuotes(m.group(1));
22+
password = m.group(2) == null ? null : stripEnclosingQuotes(m.group(2));
1723
connect = m.group(3);
1824
} else {
1925
throw new IllegalArgumentException("Not a valid connectString: '" + connectString + "'");
@@ -42,7 +48,18 @@ public String getPassword() {
4248
return password;
4349
}
4450

51+
/**
52+
* @return true when no user/password was given (connect string {@code /@<connect>}),
53+
* meaning credentials are provided externally, e.g. by an Oracle Wallet
54+
*/
55+
public boolean isExternalAuthentication() {
56+
return user == null;
57+
}
58+
4559
public String getConnectString() {
60+
if (isExternalAuthentication()) {
61+
return "/@" + connect;
62+
}
4663
return user + "/" + password + "@" + connect;
4764
}
4865

‎src/main/java/org/utplsql/cli/DataSourceProvider.java‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,32 @@
1313
*/
1414
public class DataSourceProvider {
1515

16+
private static final String TNS_ADMIN_PROPERTY = "oracle.net.tns_admin";
17+
1618
static {
17-
String oracleHome = System.getenv("ORACLE_HOME");
18-
if (oracleHome != null && System.getProperty("oracle.net.tns_admin") == null) {
19-
System.setProperty("oracle.net.tns_admin",
20-
String.join(File.separator, oracleHome, "NETWORK", "ADMIN"));
19+
String tnsAdminFallback = getTnsAdminFallback(
20+
System.getProperty(TNS_ADMIN_PROPERTY), System.getenv("TNS_ADMIN"), System.getenv("ORACLE_HOME"));
21+
if (tnsAdminFallback != null) {
22+
System.setProperty(TNS_ADMIN_PROPERTY, tnsAdminFallback);
23+
}
24+
}
25+
26+
/**
27+
* The JDBC driver resolves tnsnames.ora / ojdbc.properties from the {@value TNS_ADMIN_PROPERTY} property
28+
* or the TNS_ADMIN environment variable on its own, but it doesn't look into ORACLE_HOME.
29+
* The property takes precedence over the environment variable, so it must not be set when TNS_ADMIN is.
30+
*
31+
* @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
32+
*/
33+
static String getTnsAdminFallback(String tnsAdminProperty, String tnsAdminEnv, String oracleHome) {
34+
if (isEmpty(tnsAdminProperty) && isEmpty(tnsAdminEnv) && !isEmpty(oracleHome)) {
35+
return String.join(File.separator, oracleHome, "network", "admin");
2136
}
37+
return null;
38+
}
39+
40+
private static boolean isEmpty(String value) {
41+
return value == null || value.isEmpty();
2242
}
2343

2444
public static DataSource getDataSource(String connectString, int maxConnections) throws SQLException {

‎src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ private void setThickOrThinJdbcUrl(InitializableOracleDataSource ds) throws SQLE
4949
List<String> errors = new ArrayList<>();
5050
Throwable lastException = null;
5151

52-
ds.setUser(config.getUser());
53-
ds.setPassword(config.getPassword());
52+
// With external authentication (Oracle Wallet) the driver looks up the credentials itself
53+
if (!config.isExternalAuthentication()) {
54+
ds.setUser(config.getUser());
55+
ds.setPassword(config.getPassword());
56+
}
5457

5558
for (ConnectStringPossibility possibility : possibilities) {
5659
logger.debug("Try connecting {}", possibility.getMaskedConnectString(config));
@@ -84,15 +87,15 @@ private void setInitSqlFrom_NLS_LANG(InitializableOracleDataSource ds) {
8487
sqlCommands.add(String.format("ALTER SESSION SET NLS_TERRITORY='%s'", matcher.group(2)));
8588
}
8689

87-
if (sqlCommands.size() > 0) {
90+
if (!sqlCommands.isEmpty()) {
8891
StringBuilder sb = new StringBuilder();
8992
sb.append("BEGIN\n");
9093
for (String command : sqlCommands) {
9194
sb.append(String.format("EXECUTE IMMEDIATE q'[%s]';\n", command));
9295
}
9396
sb.append("END;");
9497

95-
logger.debug("NLS settings: {}", sb.toString());
98+
logger.debug("NLS settings: {}", sb);
9699
ds.setConnectionInitSql(sb.toString());
97100
}
98101
}
@@ -107,7 +110,7 @@ public String getConnectString(ConnectionConfig config) {
107110

108111
@Override
109112
public String getMaskedConnectString(ConnectionConfig config) {
110-
return "jdbc:oracle:oci8:****/****@" + config.getConnect();
113+
return "jdbc:oracle:oci8:" + maskedCredentials(config) + "@" + config.getConnect();
111114
}
112115
}
113116

@@ -119,7 +122,11 @@ public String getConnectString(ConnectionConfig config) {
119122

120123
@Override
121124
public String getMaskedConnectString(ConnectionConfig config) {
122-
return "jdbc:oracle:thin:****/****@" + config.getConnect();
125+
return "jdbc:oracle:thin:" + maskedCredentials(config) + "@" + config.getConnect();
123126
}
124127
}
128+
129+
private static String maskedCredentials(ConnectionConfig config) {
130+
return config.isExternalAuthentication() ? "/" : "****/****";
131+
}
125132
}

‎src/test/java/org/utplsql/cli/ConnectionConfigTest.java‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.utplsql.cli;
22

33
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.params.ParameterizedTest;
5+
import org.junit.jupiter.params.provider.ValueSource;
46

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

@@ -54,4 +56,54 @@ void parseSpecialCharsUser() {
5456
assertEquals("my.local.host/service", info.getConnect());
5557
assertFalse(info.isSysDba());
5658
}
59+
60+
@Test
61+
void parseCredentialsIsNotExternalAuthentication() {
62+
ConnectionConfig info = new ConnectionConfig("test/pw@MY_TNS_ALIAS");
63+
64+
assertFalse(info.isExternalAuthentication());
65+
assertEquals("test/pw@MY_TNS_ALIAS", info.getConnectString());
66+
}
67+
68+
@Test
69+
void parseExternalAuthentication() {
70+
ConnectionConfig info = new ConnectionConfig("/@MY_TNS_ALIAS");
71+
72+
assertNull(info.getUser());
73+
assertNull(info.getPassword());
74+
assertEquals("MY_TNS_ALIAS", info.getConnect());
75+
assertTrue(info.isExternalAuthentication());
76+
assertFalse(info.isSysDba());
77+
assertEquals("/@MY_TNS_ALIAS", info.getConnectString());
78+
}
79+
80+
@Test
81+
void parseExternalAuthenticationWithTnsAdminInUrl() {
82+
ConnectionConfig info = new ConnectionConfig("/@MY_TNS_ALIAS?TNS_ADMIN=/home/me/oracle/network/admin");
83+
84+
assertNull(info.getUser());
85+
assertNull(info.getPassword());
86+
assertEquals("MY_TNS_ALIAS?TNS_ADMIN=/home/me/oracle/network/admin", info.getConnect());
87+
assertTrue(info.isExternalAuthentication());
88+
}
89+
90+
@Test
91+
void parseExternalAuthenticationWithEzConnect() {
92+
ConnectionConfig info = new ConnectionConfig("/@//my.local.host:1521/service");
93+
94+
assertEquals("//my.local.host:1521/service", info.getConnect());
95+
assertTrue(info.isExternalAuthentication());
96+
}
97+
98+
@ParameterizedTest
99+
@ValueSource(strings = {
100+
"/pw@MY_TNS_ALIAS", // password without user
101+
"test/@MY_TNS_ALIAS", // user without password
102+
"@MY_TNS_ALIAS",
103+
"test@MY_TNS_ALIAS",
104+
"MY_TNS_ALIAS"
105+
})
106+
void rejectInvalidConnectString(String connectString) {
107+
assertThrows(IllegalArgumentException.class, () -> new ConnectionConfig(connectString));
108+
}
57109
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.utplsql.cli;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.File;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertNull;
9+
10+
class DataSourceProviderTest {
11+
12+
private static final String ORACLE_HOME = "/opt/oracle";
13+
14+
@Test
15+
void tnsAdminFallsBackToOracleHome() {
16+
assertEquals(String.join(File.separator, ORACLE_HOME, "network", "admin"),
17+
DataSourceProvider.getTnsAdminFallback(null, null, ORACLE_HOME));
18+
}
19+
20+
@Test
21+
void tnsAdminEnvVariableIsLeftToJdbcDriver() {
22+
// Setting the property would override the TNS_ADMIN environment variable in the JDBC driver
23+
assertNull(DataSourceProvider.getTnsAdminFallback(null, "/my/tns_admin", ORACLE_HOME));
24+
}
25+
26+
@Test
27+
void tnsAdminPropertyIsNotOverridden() {
28+
assertNull(DataSourceProvider.getTnsAdminFallback("/my/tns_admin", null, ORACLE_HOME));
29+
}
30+
31+
@Test
32+
void noFallbackWithoutOracleHome() {
33+
assertNull(DataSourceProvider.getTnsAdminFallback(null, null, null));
34+
assertNull(DataSourceProvider.getTnsAdminFallback("", "", ""));
35+
}
36+
}

0 commit comments

Comments
 (0)