Skip to content

Commit 5e82756

Browse files
committed
Adding support for database connections via Oracle Wallet.
1 parent 7bd004c commit 5e82756

7 files changed

Lines changed: 229 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,42 @@ 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
103+
To connect using TNS, you need to have either the TNS_ADMIN or the ORACLE_HOME environment variable set.
104+
The file tnsnames.ora must exist in the TNS_ADMIN directory or in %ORACLE_HOME%/network/admin
104105
The file tnsnames.ora must contain valid TNS entries.
105106

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

112+
#### Oracle Wallet (Secure External Password Store)
113+
114+
To avoid passing the password on the command line, store the credentials in an Oracle Wallet and connect with `/@<TNSName>`:
115+
```
116+
utplsql run /@MYDATABASE
117+
```
118+
119+
Setup example:
120+
```
121+
# create an auto-login wallet with credentials for TNS alias MYDATABASE
122+
orapki wallet create -wallet $HOME/oracle/wallet -auto_login_local
123+
mkstore -wrl $HOME/oracle/wallet -createCredential MYDATABASE someusername
124+
125+
# point the JDBC driver to the wallet
126+
echo "oracle.net.wallet_location=(SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY=$HOME/oracle/wallet)))" \
127+
> $HOME/oracle/network/admin/ojdbc.properties
128+
129+
# tnsnames.ora with the MYDATABASE entry must be in the same directory
130+
export TNS_ADMIN=$HOME/oracle/network/admin
131+
```
132+
133+
Instead of setting `TNS_ADMIN` you can also pass it as part of the connect string: `/@MYDATABASE?TNS_ADMIN=/path/to/network/admin`.
134+
135+
The TNS alias used in the connect string must match the alias of the credential stored in the wallet.
136+
111137
### run
112138
`utplsql run <ConnectionURL> [<options>]`
113139

‎pom.xml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<maven.compiler.release>17</maven.compiler.release>
1616

1717
<utplsql-java-api.version>3.2.4</utplsql-java-api.version>
18+
<!-- Temporarly here - it needs to be moved to java-api. Keep in sync with the ojdbc version used by utplsql-java-api -->
19+
<oraclepki.version>23.7.0.25.01</oraclepki.version>
1820

1921
<junit.jupiter.version>5.12.2</junit.jupiter.version>
2022
<picocli.version>4.7.7</picocli.version>
@@ -73,6 +75,12 @@
7375
<artifactId>utplsql-java-api</artifactId>
7476
<version>${utplsql-java-api.version}</version>
7577
</dependency>
78+
<!-- Temporarly here - it needs to be moved to java-api. Required by ojdbc to read Oracle Wallets (Secure External Password Store) -->
79+
<dependency>
80+
<groupId>com.oracle.database.security</groupId>
81+
<artifactId>oraclepki</artifactId>
82+
<version>${oraclepki.version}</version>
83+
</dependency>
7684
<dependency>
7785
<groupId>javax.xml.bind</groupId>
7886
<artifactId>jaxb-api</artifactId>

‎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: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
public class DataSourceProvider {
1515

1616
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"));
17+
if (System.getProperty("oracle.net.tns_admin") == null) {
18+
String tnsAdmin = System.getenv("TNS_ADMIN");
19+
String oracleHome = System.getenv("ORACLE_HOME");
20+
if (tnsAdmin != null && !tnsAdmin.isEmpty()) {
21+
System.setProperty("oracle.net.tns_admin", tnsAdmin);
22+
} else if (oracleHome != null) {
23+
System.setProperty("oracle.net.tns_admin",
24+
String.join(File.separator, oracleHome, "NETWORK", "ADMIN"));
25+
}
2126
}
2227
}
2328

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

Lines changed: 11 additions & 4 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));
@@ -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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package org.utplsql.cli;
2+
3+
import oracle.security.pki.OracleSecretStore;
4+
import oracle.security.pki.OracleWallet;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.io.TempDir;
8+
import org.utplsql.cli.datasource.TestedDataSourceProvider;
9+
10+
import javax.sql.DataSource;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.sql.Connection;
14+
import java.sql.ResultSet;
15+
import java.sql.Statement;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
22+
23+
/**
24+
* Connecting with {@code /@<TNS alias>} using credentials stored in an Oracle Wallet
25+
* (Secure External Password Store), see issue #225.
26+
* <p>
27+
* The wallet, tnsnames.ora and ojdbc.properties are created on the fly from DB_URL / DB_USER / DB_PASS,
28+
* so no Oracle client tooling (mkstore/orapki) is needed.
29+
*/
30+
class WalletConnectionIT {
31+
32+
private static final String TNS_ALIAS = "UTPLSQL_CLI_WALLET";
33+
private static final Pattern EZ_CONNECT = Pattern.compile("^//([^:/]+)(?::(\\d+))?/(.+)$");
34+
35+
@TempDir
36+
static Path tnsAdmin;
37+
38+
private static String walletConnectString;
39+
40+
@BeforeAll
41+
static void createWallet() throws Exception {
42+
Matcher m = EZ_CONNECT.matcher(TestHelper.getUrl());
43+
assumeTrue(m.matches(), "DB_URL must be in //host[:port]/service format to generate tnsnames.ora");
44+
String host = m.group(1);
45+
String port = m.group(2) == null ? "1521" : m.group(2);
46+
String service = m.group(3);
47+
48+
Path walletDir = Files.createDirectories(tnsAdmin.resolve("wallet"));
49+
char[] walletPassword = "Wallet_Pwd_123".toCharArray();
50+
51+
OracleWallet wallet = new OracleWallet();
52+
wallet.create(walletPassword);
53+
OracleSecretStore secretStore = wallet.getSecretStore();
54+
secretStore.createCredential(TNS_ALIAS.toCharArray(),
55+
TestHelper.getUser().toCharArray(),
56+
TestHelper.getPass().toCharArray());
57+
wallet.setSecretStore(secretStore);
58+
wallet.saveAs(walletDir.toString());
59+
wallet.createSSO();
60+
wallet.saveSSO();
61+
62+
assertTrue(Files.exists(walletDir.resolve("cwallet.sso")), "auto-login wallet was not created");
63+
64+
Files.writeString(tnsAdmin.resolve("tnsnames.ora"),
65+
TNS_ALIAS + " = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = " + host + ")(PORT = " + port + "))"
66+
+ "(CONNECT_DATA = (SERVICE_NAME = " + service + ")))\n");
67+
Files.writeString(tnsAdmin.resolve("ojdbc.properties"),
68+
"oracle.net.wallet_location=(SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY="
69+
+ forwardSlashes(walletDir) + ")))\n");
70+
71+
walletConnectString = "/@" + TNS_ALIAS + "?TNS_ADMIN=" + forwardSlashes(tnsAdmin);
72+
}
73+
74+
private static String forwardSlashes(Path path) {
75+
return path.toAbsolutePath().toString().replace('\\', '/');
76+
}
77+
78+
@Test
79+
void connectsAsWalletUser() throws Exception {
80+
ConnectionConfig config = new ConnectionConfig(walletConnectString);
81+
DataSource dataSource = new TestedDataSourceProvider(config, 1).getDataSource();
82+
83+
try (Connection con = dataSource.getConnection();
84+
Statement stmt = con.createStatement();
85+
ResultSet rs = stmt.executeQuery("select user from dual")) {
86+
assertTrue(rs.next());
87+
assertEquals(TestHelper.getUser().toUpperCase(), rs.getString(1));
88+
}
89+
}
90+
91+
@Test
92+
void runCommandWithWallet() {
93+
int result = TestHelper.runApp("run",
94+
walletConnectString,
95+
"-f=ut_documentation_reporter",
96+
"-s",
97+
"--failure-exit-code=0");
98+
99+
assertEquals(0, result);
100+
}
101+
}

0 commit comments

Comments
 (0)