Skip to content

Commit 2cfaff3

Browse files
committed
Config Provider support improvements and more test cases
1 parent bc00fa2 commit 2cfaff3

File tree

10 files changed

+297
-135
lines changed

10 files changed

+297
-135
lines changed

doc/src/api_manual/oracledb.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,21 @@ Each of the configuration properties is described below.
749749
const oracledb = require('oracledb');
750750
oracledb.autoCommit = false;
751751
752+
.. attribute:: oracledb.configProviderCacheTimeout
753+
754+
This property is the number of seconds that node-oracledb keeps
755+
the configuration information retrieved from a
756+
:ref:`centralized configuration provider <configurationprovider>` cached.
757+
758+
The default value is *86400* seconds.
759+
760+
**Example**
761+
762+
.. code-block:: javascript
763+
764+
const oracledb = require('oracledb');
765+
oracledb.configProviderCacheTimeout = 6;
766+
752767
.. attribute:: oracledb.connectionClass
753768

754769
The user-chosen Connection class value is a string which defines a

doc/src/release_notes.rst

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,21 @@ node-oracledb `v6.7.0 <https://github.com/oracle/node-oracledb/compare/v6.6.0...
1313
Common Changes
1414
++++++++++++++
1515

16-
Thin Mode Changes
17-
+++++++++++++++++
16+
#) Added support to connect to Oracle Database using wallets stored in
17+
Azure Key Vault and OCI Vault.
18+
19+
#) Added ability to cache the configuration information retrieved from
20+
:ref:`Azure App Configuration <conninfocacheazure>` and
21+
:ref:`OCI Object Storage <conninfocacheoci>` centralized configuration
22+
providers.
23+
24+
#) Ensure that the password stored in OCI vault and retrieved
25+
in base64-encoded format is decoded correctly.
1826

1927
#) Changed default values of ``transportConnectTimeout`` and
20-
``retryDelay`` properties in :meth:`oracledb.getConnection()` and
21-
:meth:`oracledb.createPool()` for consistency with other Oracle Database
22-
drivers.
28+
``retryDelay`` properties to *20* seconds and *1* second respectively in
29+
:meth:`oracledb.getConnection()` and :meth:`oracledb.createPool()` for
30+
consistency with other Oracle Database drivers.
2331

2432
#) Changed the password type parameter values from `vault-oci` and
2533
`vault-azure` to `ocivault` and `azurevault` respectively for consistency
@@ -28,6 +36,9 @@ Thin Mode Changes
2836
#) Added method :meth:`oracledb.getNetworkServiceNames()` to support fetching
2937
the list of network service names from the ``tnsnames.ora`` file.
3038

39+
Thin Mode Changes
40+
+++++++++++++++++
41+
3142
#) Fixed bug that did not allow connection to Oracle Database 23ai instances
3243
that have fast authentication disabled.
3344

doc/src/user_guide/connection_handling.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4406,6 +4406,22 @@ the order of precedence from highest to lowest will be as follows:
44064406
- Azure App Configuration
44074407
- Application
44084408
4409+
.. _conninfocacheazure:
4410+
4411+
Azure App Configuration Information Caching
4412+
+++++++++++++++++++++++++++++++++++++++++++
4413+
4414+
Node-oracledb caches configuration information from Azure App Configuration by
4415+
default. This allows you to reuse the cached configuration information which
4416+
significantly reduces the number of round-trips to this configuration
4417+
provider.
4418+
4419+
You can use the :attr:`oracledb.configProviderCacheTimeout` property to set
4420+
the amount of time for node-oracledb to cache the configuration retrieved from
4421+
Azure App Configuration. Once the cache expires, node-oracledb refreshes the
4422+
cache when configuration information from this configuration provider is
4423+
required.
4424+
44094425
.. _ociobjstorage:
44104426
44114427
OCI Object Storage Configuration Provider
@@ -4692,6 +4708,21 @@ precedence from highest to lowest will be as follows:
46924708
- OCI Object Storage
46934709
- Application
46944710
4711+
.. _conninfocacheoci:
4712+
4713+
OCI Object Storage Configuration Information Caching
4714+
++++++++++++++++++++++++++++++++++++++++++++++++++++
4715+
4716+
Node-oracledb caches configuration information from OCI Object Storage by
4717+
default. This allows you to reuse the cached configuration information which
4718+
significantly reduces the number of round-trips to this configuration
4719+
provider.
4720+
4721+
You can use the :attr:`oracledb.configProviderCacheTimeout` property to set
4722+
the amount of time for node-oracledb to cache the configuration retrieved from
4723+
OCI Object Storage. Once the cache expires, node-oracledb refreshes the cache
4724+
when configuration information from this configuration provider is required.
4725+
46954726
.. _sharding:
46964727
46974728
Connecting to Sharded Databases

lib/configProviders/azure.js

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ class AzureProvider extends base {
112112
async returnConfig() {
113113
const configObject = {};
114114
const label = this.paramMap.get("label");
115-
const credential = this._returnCredential();
115+
this.credential = this._returnCredential();
116116
// azure config store
117117
const client = new AppConfigurationClient(
118118
"https://" + this.paramMap.get("appconfigname"), // ex: <https://<your appconfig resource>.azconfig.io>
119-
credential
119+
this.credential
120120
);
121121
// retrieve connect_description
122122
configObject.connectString = (await this._getConfigurationSetting(client, this.paramMap.get("key") + 'connect_descriptor', label)).value;
@@ -139,29 +139,40 @@ class AzureProvider extends base {
139139
configObject.user = null;
140140
}
141141
//retrieve password
142-
let pwdJson = null;
142+
configObject.password = await this.retrieveParamValueFromVault(client, label, 'password');
143+
// retrieve wallet_location
144+
configObject.walletContent = await this.retrieveParamValueFromVault(client, label, 'wallet_location');
145+
if (configObject.walletContent) {
146+
//only Pem file supported
147+
if (!this.isPemFile(configObject.walletContent))
148+
errors.throwErr(errors.ERR_WALLET_TYPE_NOT_SUPPORTED);
149+
}
150+
return configObject;
151+
}
152+
async retrieveParamValueFromVault(client, label, param) {
153+
let paramJson = null;
143154
try {
144-
pwdJson = await this._getConfigurationSetting(client, this.paramMap.get("key") + 'password', label);
155+
paramJson = await this._getConfigurationSetting(client, this.paramMap.get("key") + param, label);
145156
} catch {
146-
configObject.password = null;
157+
return null;
147158
}
148-
if (pwdJson) {
159+
if (paramJson) {
149160
let obj;
150161
try {
151-
obj = JSON.parse(pwdJson.value);
162+
obj = JSON.parse(paramJson.value);
152163
} catch {
153-
obj = pwdJson.value;
164+
obj = paramJson.value;
154165
}
155166
if (obj.uri) {
156167
const { SecretClient } = require("@azure/keyvault-secrets");
157168
const vault_detail = await this._parsePwd(obj.uri);
158-
const client1 = new SecretClient(vault_detail[0], credential);
159-
configObject.password = (await client1.getSecret(vault_detail[1])).value;
169+
const client1 = new SecretClient(vault_detail[0], this.credential);
170+
return (await client1.getSecret(vault_detail[1])).value;
160171
} else {
161-
configObject.password = pwdJson.value;
172+
return paramJson.value;
162173
}
163174
}
164-
return configObject;
175+
165176
}
166177
}
167178
module.exports = AzureProvider;

lib/configProviders/base.js

Lines changed: 23 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,49 +26,35 @@
2626

2727
'use strict';
2828

29+
const pemHeaders = [
30+
'-----BEGIN CERTIFICATE-----',
31+
'-----BEGIN PUBLIC KEY-----',
32+
'-----BEGIN PRIVATE KEY-----',
33+
'-----BEGIN RSA PRIVATE KEY-----',
34+
'-----BEGIN DSA PRIVATE KEY-----',
35+
'-----BEGIN EC PRIVATE KEY-----',
36+
'-----BEGIN ENCRYPTED PRIVATE KEY-----'
37+
];
38+
const pemFooters = [
39+
'-----END CERTIFICATE-----',
40+
'-----END PUBLIC KEY-----',
41+
'-----END PRIVATE KEY-----',
42+
'-----END RSA PRIVATE KEY-----',
43+
'-----END DSA PRIVATE KEY-----',
44+
'-----END EC PRIVATE KEY-----',
45+
'-----END ENCRYPTED PRIVATE KEY-----'
46+
];
2947
class base {
3048
constructor(url) {
3149
const params = new URLSearchParams(url);
3250
this.paramMap = new URLSearchParams([...params].map(([key, value]) => [key.toLowerCase(), value])); //parse the extended part and store parameters in Map
3351
}
3452

35-
/**
36-
* Sets precedence for different parameters in cloudConfig/userConfig
37-
* @param {cloudConfig} object - object retreived from cloud
38-
* @param {userConfig} object - user input Object
39-
*/
40-
modifyOptionsPrecedence(cloudConfig, userConfig) {
41-
// create a copy of userConfig object
42-
userConfig = { ...userConfig };
43-
if (!userConfig.user)
44-
userConfig.user = cloudConfig.user;
45-
if (!userConfig.password)
46-
userConfig.password = cloudConfig.password;
47-
if (cloudConfig.connectString) {
48-
userConfig.connectString = cloudConfig.connectString;
49-
userConfig.connectionString = undefined;
50-
}
51-
if (cloudConfig.poolMin)
52-
userConfig.poolMin = cloudConfig.poolMin;
53-
if (cloudConfig.poolMax)
54-
userConfig.poolMax = cloudConfig.poolMax;
55-
if (cloudConfig.poolIncrement)
56-
userConfig.poolIncrement = cloudConfig.poolIncrement;
57-
if (cloudConfig.poolTimeout)
58-
userConfig.poolTimeout = cloudConfig.poolTimeout;
59-
if (cloudConfig.poolPingInterval)
60-
userConfig.poolPingInterval = cloudConfig.poolPingInterval;
61-
if (cloudConfig.poolPingTimeout)
62-
userConfig.poolPingTimeout = cloudConfig.poolPingTimeout;
63-
if (cloudConfig.stmtCacheSize)
64-
userConfig.stmtCacheSize = cloudConfig.stmtCacheSize;
65-
if (cloudConfig.prefetchRows)
66-
userConfig.prefetchRows = cloudConfig.prefetchRows;
67-
if (cloudConfig.lobPrefetch)
68-
userConfig.lobPrefetch = cloudConfig.lobPrefetch;
69-
70-
return userConfig;
71-
53+
isPemFile(content) {
54+
//remove any line breaks at the end of the content
55+
content = content.trim();
56+
return pemHeaders.some(header => content.includes(header)) &&
57+
pemFooters.some(footer => content.endsWith(footer));
7258
}
7359

7460
//---------------------------------------------------------------------------

lib/configProviders/ociobject.js

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const fs = require('fs');
3232
const cloud_net_naming_pattern_oci = new RegExp("(?<objservername>[A-Za-z0-9._-]+)/n/" + "(?<namespace>[A-Za-z0-9._-]+)/b/" + "(?<bucketname>[A-Za-z0-9._-]+)/o/" + "(?<filename>[A-Za-z0-9._-]+)" + "(/c/(?<alias>.+))?$");
3333
// object to store module references that will be populated by init()
3434
const oci = {};
35-
3635
class OCIProvider extends base {
3736
constructor(provider_arg, urlExtendedPart) {
3837
super(urlExtendedPart);
@@ -127,65 +126,41 @@ class OCIProvider extends base {
127126
//---------------------------------------------------------------------------
128127
async returnConfig() {
129128
const configObject = {};
130-
const credential = await this._returnCredential();
129+
this.credential = await this._returnCredential();
131130
// oci object store
132131
const client_oci = new (oci.objectstorage).ObjectStorageClient({
133-
authenticationDetailsProvider: credential
132+
authenticationDetailsProvider: this.credential
134133
});
135134
const getObjectRequest = {
136135
objectName: this.paramMap.get('filename'),
137136
bucketName: this.paramMap.get('bucketname'),
138137
namespaceName: this.paramMap.get('namespace')
139138
};
140-
let credential1;
141139
const getObjectResponse = await client_oci.getObject(getObjectRequest);
142140
const resp = await this._streamToString(getObjectResponse.value);
143141
// Entire object we get from OCI Object Storage
144-
let obj = JSON.parse(resp);
142+
this.obj = JSON.parse(resp);
145143
const userAlias = this.paramMap.get('alias');
146144
if (userAlias) {
147-
obj = obj[userAlias];
145+
this.obj = this.obj[userAlias];
148146
}
149147
const pmSection = 'node-oracledb';
150-
const params = obj[pmSection];
148+
const params = this.obj[pmSection];
151149
for (const key in params) {
152150
var val = params[key];
153151
configObject[key] = val;
154152
}
155-
configObject.connectString = obj.connect_descriptor;
156-
configObject.user = obj.user;
157-
if (obj.password) {
158-
if (obj.password.type == "azurevault") {
159-
if (obj.password.authentication) {
160-
const { SecretClient } = require("@azure/keyvault-secrets");
161-
const {ClientSecretCredential, ClientCertificateCredential} = require("@azure/identity");
162-
if (obj.password.authentication.azure_client_secret)
163-
credential1 = new ClientSecretCredential(obj.password.authentication.azure_tenant_id, obj.password.authentication.azure_client_id, obj.password.authentication.azure_client_secret);
164-
else if (obj.password.authentication.azure_client_certificate_path)
165-
credential1 = new ClientCertificateCredential(obj.password.authentication.azure_tenant_id, obj.password.authentication.azure_client_id, obj.password.authentication.azure_client_certificate_path);
166-
else
167-
errors.throwErr(errors.ERR_AZURE_VAULT_AUTH_FAILED);
168-
const vault_detail = await this._parsePwd(obj.password.value);
169-
const client1 = new SecretClient(vault_detail[0], credential1);
170-
configObject.password = (await client1.getSecret(vault_detail[1])).value;
171-
} else {
172-
errors.throwErr(errors.ERR_AZURE_VAULT_AUTH_FAILED);
173-
}
174-
} else if (obj.password.type == "ocivault") {
175-
const secrets = require('oci-secrets');
176-
const secretClientOci = new secrets.SecretsClient({
177-
authenticationDetailsProvider: credential
178-
});
179-
const getSecretBundleRequest = {
180-
secretId: obj.password.value
181-
};
182-
const getSecretBundleResponse = await secretClientOci.getSecretBundle(getSecretBundleRequest);
183-
configObject.password = getSecretBundleResponse.secretBundle.secretBundleContent.content;
184-
} else if (obj.password) {
185-
configObject.password = obj.password;
186-
}
187-
} else {
188-
configObject.password = null;
153+
configObject.connectString = this.obj.connect_descriptor;
154+
configObject.user = this.obj.user;
155+
if (this.obj.password) {
156+
configObject.password = await this.retrieveParamValueFromVault('password');
157+
}
158+
if (this.obj.wallet_location) {
159+
// retrieve wallet_location
160+
configObject.walletContent = await this.retrieveParamValueFromVault('wallet_location');
161+
//only Pem file supported
162+
if (!this.isPemFile(configObject.walletContent))
163+
errors.throwErr(errors.ERR_WALLET_TYPE_NOT_SUPPORTED);
189164
}
190165
return configObject;
191166
}
@@ -199,5 +174,40 @@ class OCIProvider extends base {
199174
const arr = objservername.split(".");
200175
return arr[1].toUpperCase().replaceAll('-', '_');
201176
}
177+
178+
async retrieveParamValueFromVault(param) {
179+
if (this.obj[param].type == "azurevault") {
180+
if (this.obj[param].authentication) {
181+
const { SecretClient } = require("@azure/keyvault-secrets");
182+
const {ClientSecretCredential, ClientCertificateCredential} = require("@azure/identity");
183+
if (this.obj[param].authentication.azure_client_secret)
184+
this.credential = new ClientSecretCredential(this.obj[param].authentication.azure_tenant_id, this.obj[param].authentication.azure_client_id, this.obj[param].authentication.azure_client_secret);
185+
else if (this.obj[param].authentication.azure_client_certificate_path)
186+
this.credential = new ClientCertificateCredential(this.obj[param].authentication.azure_tenant_id, this.obj[param].authentication.azure_client_id, this.obj[param].authentication.azure_client_certificate_path);
187+
else
188+
errors.throwErr(errors.ERR_AZURE_VAULT_AUTH_FAILED);
189+
const vault_detail = await this._parsePwd(this.obj[param].value);
190+
const client1 = new SecretClient(vault_detail[0], this.credential);
191+
return (await client1.getSecret(vault_detail[1])).value;
192+
} else {
193+
errors.throwErr(errors.ERR_AZURE_VAULT_AUTH_FAILED);
194+
}
195+
} else if (this.obj[param].type == "ocivault") {
196+
const secrets = require('oci-secrets');
197+
const secretClientOci = new secrets.SecretsClient({
198+
authenticationDetailsProvider: this.credential
199+
});
200+
const getSecretBundleRequest = {
201+
secretId: this.obj[param].value
202+
};
203+
const getSecretBundleResponse = await secretClientOci.getSecretBundle(getSecretBundleRequest);
204+
const base64content = getSecretBundleResponse.secretBundle.secretBundleContent.content;
205+
// decode base64 content
206+
const returnVal = Buffer.from(base64content, "base64").toString("utf-8");
207+
return returnVal;
208+
} else {
209+
return this.obj[param];
210+
}
211+
}
202212
}
203213
module.exports = OCIProvider;

lib/errors.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const ERR_CONFIG_PROVIDER_LOAD_FAILED = 525;
193193
const ERR_OCIOBJECT_CONFIG_PROVIDER_AUTH_FAILED = 526;
194194
const ERR_AZURE_VAULT_AUTH_FAILED = 527;
195195
const ERR_AZURE_SERVICE_PRINCIPAL_AUTH_FAILED = 528;
196+
const ERR_WALLET_TYPE_NOT_SUPPORTED = 529;
196197

197198
// Oracle SUCCESS_WITH_INFO warning start from 700
198199
const WRN_COMPILATION_CREATE = 700;
@@ -521,6 +522,7 @@ messages.set(ERR_AZURE_VAULT_AUTH_FAILED, // NJS-527
521522
'Azure Vault: Provide correct azure vault authentication details');
522523
messages.set(ERR_AZURE_SERVICE_PRINCIPAL_AUTH_FAILED, // NJS-528
523524
'Azure service principal authentication requires either a client certificate path or a client secret string');
525+
messages.set(ERR_WALLET_TYPE_NOT_SUPPORTED, 'Invalid wallet content format. Supported format is PEM');// NJS-529
524526
// Oracle SUCCESS_WITH_INFO warning
525527

526528
messages.set(WRN_COMPILATION_CREATE, // NJS-700
@@ -837,6 +839,7 @@ module.exports = {
837839
ERR_CONFIG_PROVIDER_FAILED_TO_RETRIEVE_CONFIG,
838840
ERR_CONFIG_PROVIDER_NOT_SUPPORTED,
839841
ERR_CONFIG_PROVIDER_LOAD_FAILED,
842+
ERR_WALLET_TYPE_NOT_SUPPORTED,
840843
ERR_INVALID_BIND_NAME,
841844
ERR_WRONG_NUMBER_OF_POSITIONAL_BINDS,
842845
ERR_BUFFER_LENGTH_INSUFFICIENT,

0 commit comments

Comments
 (0)