oracledb 7.0.1 → 26.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/build/Release/oracledb-26.0.0-darwin-arm64.node +0 -0
- package/build/Release/oracledb-26.0.0-darwin-arm64.node-buildinfo.txt +1 -0
- package/build/Release/oracledb-26.0.0-js-buildinfo.txt +1 -0
- package/build/Release/oracledb-26.0.0-linux-arm64.node +0 -0
- package/build/Release/oracledb-26.0.0-linux-arm64.node-buildinfo.txt +1 -0
- package/build/Release/oracledb-26.0.0-linux-x64.node +0 -0
- package/build/Release/oracledb-26.0.0-linux-x64.node-buildinfo.txt +1 -0
- package/build/Release/oracledb-26.0.0-win32-x64.node +0 -0
- package/build/Release/oracledb-26.0.0-win32-x64.node-buildinfo.txt +1 -0
- package/lib/connection.js +71 -8
- package/lib/constants.js +4 -0
- package/lib/endUserSecurityContext.js +20 -8
- package/lib/errors.js +70 -5
- package/lib/impl/connection.js +36 -0
- package/lib/impl/datahandlers/buffer.js +91 -18
- package/lib/impl/datahandlers/oson.js +3 -2
- package/lib/impl/resultset.js +32 -11
- package/lib/oracledb.js +34 -0
- package/lib/pool.js +6 -2
- package/lib/queryStream.js +13 -5
- package/lib/resultset.js +10 -3
- package/lib/securityContextProvider.js +206 -0
- package/lib/thin/aq.js +2 -3
- package/lib/thin/connection.js +381 -19
- package/lib/thin/dbObject.js +17 -8
- package/lib/thin/pool.js +3 -2
- package/lib/thin/protocol/capabilities.js +37 -4
- package/lib/thin/protocol/constants.js +49 -1
- package/lib/thin/protocol/messages/auth.js +17 -6
- package/lib/thin/protocol/messages/base.js +74 -16
- package/lib/thin/protocol/messages/dataType.js +46 -0
- package/lib/thin/protocol/messages/directPathLoadStream.js +3 -2
- package/lib/thin/protocol/messages/execute.js +10 -4
- package/lib/thin/protocol/messages/index.js +4 -0
- package/lib/thin/protocol/messages/notification.js +281 -0
- package/lib/thin/protocol/messages/subscribe.js +181 -0
- package/lib/thin/protocol/messages/withData.js +21 -7
- package/lib/thin/protocol/packet.js +2 -4
- package/lib/thin/protocol/protocol.js +7 -4
- package/lib/thin/resultSet.js +40 -20
- package/lib/thin/sqlnet/ezConnectResolver.js +7 -0
- package/lib/thin/sqlnet/navNodes.js +3 -3
- package/lib/thin/sqlnet/ntTcp.js +24 -8
- package/lib/thin/statement.js +6 -1
- package/lib/thin/statementCache.js +34 -6
- package/lib/thin/subscr.js +363 -0
- package/lib/transformer.js +60 -17
- package/lib/util.js +46 -8
- package/lib/version.js +2 -2
- package/package.json +1 -1
- package/plugins/configProviders/gcpstorage/index.js +121 -0
- package/plugins/configProviders/ociobject/index.js +1 -1
- package/plugins/token/cache.js +154 -0
- package/plugins/token/endUserSecurityProvider/index.js +377 -0
- package/plugins/token/extensionAzure/index.js +160 -28
- package/plugins/token/extensionOci/index.js +221 -16
- package/plugins/vectorsdk/OracleVecDB.js +346 -0
- package/plugins/vectorsdk/index.js +49 -0
- package/plugins/vectorsdk/utils.js +370 -0
- package/plugins/vectorsdk/vectorEmbedding.js +223 -0
- package/plugins/vectorsdk/vectorIndex.js +177 -0
- package/plugins/vectorsdk/vectorRecords.js +406 -0
- package/plugins/vectorsdk/vectorSchema.js +408 -0
- package/plugins/vectorsdk/vectorSearch.js +538 -0
- package/build/Release/oracledb-7.0.1-darwin-arm64.node +0 -0
- package/build/Release/oracledb-7.0.1-darwin-arm64.node-buildinfo.txt +0 -1
- package/build/Release/oracledb-7.0.1-js-buildinfo.txt +0 -1
- package/build/Release/oracledb-7.0.1-linux-arm64.node +0 -0
- package/build/Release/oracledb-7.0.1-linux-arm64.node-buildinfo.txt +0 -1
- package/build/Release/oracledb-7.0.1-linux-x64.node +0 -0
- package/build/Release/oracledb-7.0.1-linux-x64.node-buildinfo.txt +0 -1
- package/build/Release/oracledb-7.0.1-win32-x64.node +0 -0
- package/build/Release/oracledb-7.0.1-win32-x64.node-buildinfo.txt +0 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// Copyright (c) 2026, Oracle and/or its affiliates.
|
|
2
|
+
|
|
3
|
+
//-----------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// This software is dual-licensed to you under the Universal Permissive License
|
|
6
|
+
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
|
|
7
|
+
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
|
|
8
|
+
// either license.
|
|
9
|
+
//
|
|
10
|
+
// If you elect to accept the software under the Apache License, Version 2.0,
|
|
11
|
+
// the following applies:
|
|
12
|
+
//
|
|
13
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
14
|
+
// you may not use this file except in compliance with the License.
|
|
15
|
+
// You may obtain a copy of the License at
|
|
16
|
+
//
|
|
17
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
18
|
+
//
|
|
19
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
20
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
21
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
22
|
+
// See the License for the specific language governing permissions and
|
|
23
|
+
// limitations under the License.
|
|
24
|
+
//
|
|
25
|
+
//-----------------------------------------------------------------------------
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* End-user security context token provider. Each configured pool or standalone
|
|
30
|
+
* connection owns its provider; there is no process-global provider setting.
|
|
31
|
+
*/
|
|
32
|
+
const oracledb = require('oracledb');
|
|
33
|
+
const securityContextProvider = oracledb.getSecurityContextProvider();
|
|
34
|
+
|
|
35
|
+
const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
|
|
36
|
+
// These endUserSecParams fields are consumed by this provider and must not be
|
|
37
|
+
// forwarded to an Azure or OCI token extension as token configuration.
|
|
38
|
+
const PROVIDER_CONTROL_FIELDS = new Set([
|
|
39
|
+
'spiType', 'authFlow', 'endUserToken', 'endUserName',
|
|
40
|
+
'dataRoles', 'attributes', 'key',
|
|
41
|
+
'cacheOptions', 'contextResolution'
|
|
42
|
+
]);
|
|
43
|
+
const DEFAULT_METADATA_FIELDS = [
|
|
44
|
+
'endUserToken', 'endUserName', 'dataRoles', 'attributes', 'key',
|
|
45
|
+
];
|
|
46
|
+
const PROVIDER_MODULE_PATHS = {
|
|
47
|
+
azure: '../extensionAzure/index.js',
|
|
48
|
+
oci: '../extensionOci/index.js',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function throwErr(message, code) {
|
|
52
|
+
const err = new Error(message);
|
|
53
|
+
if (code) {
|
|
54
|
+
err.code = code;
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureInvocationHook() {
|
|
60
|
+
if (!securityContextProvider.getInvocationHook?.()) {
|
|
61
|
+
securityContextProvider.setInvocationHook(
|
|
62
|
+
securityContextProvider.createInvocationHook()
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeAuthFlow(value) {
|
|
68
|
+
const flow = String(value ?? 'onBehalfOf').trim().toLowerCase();
|
|
69
|
+
if (flow === 'app' || flow === 'clientcredentials' ||
|
|
70
|
+
flow === 'client_credentials' || flow === 'client-credentials') {
|
|
71
|
+
return 'app';
|
|
72
|
+
}
|
|
73
|
+
if (flow === 'obo' || flow === 'onbehalfof' ||
|
|
74
|
+
flow === 'on_behalf_of' || flow === 'on-behalf-of') {
|
|
75
|
+
return 'obo';
|
|
76
|
+
}
|
|
77
|
+
throwErr("endUserSecParams.authFlow must be 'app' or 'onBehalfOf'.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeContextResolution(value) {
|
|
81
|
+
const resolution = String(value ?? 'connection').trim().toLowerCase();
|
|
82
|
+
if (resolution === 'connection' || resolution === 'operation') {
|
|
83
|
+
return resolution;
|
|
84
|
+
}
|
|
85
|
+
throwErr("endUserSecParams.contextResolution must be 'connection' or 'operation'.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeProvider(params) {
|
|
89
|
+
const spiType = String(params.spiType ?? 'oci').trim().toLowerCase();
|
|
90
|
+
const modulePath = PROVIDER_MODULE_PATHS[spiType];
|
|
91
|
+
if (!modulePath) {
|
|
92
|
+
throwErr("endUserSecParams.spiType must be 'azure' or 'oci'.");
|
|
93
|
+
}
|
|
94
|
+
// Start with the supplied parameters, then remove fields interpreted by this
|
|
95
|
+
// end-user-security provider. The remaining tokenConfig is passed only to
|
|
96
|
+
// the selected Azure or OCI extension, so request metadata and provider
|
|
97
|
+
// controls cannot accidentally be treated as token-client options.
|
|
98
|
+
const tokenConfig = { ...params };
|
|
99
|
+
for (const key of PROVIDER_CONTROL_FIELDS) {
|
|
100
|
+
delete tokenConfig[key];
|
|
101
|
+
}
|
|
102
|
+
// Azure's application-token helper expects this value. It is harmless for
|
|
103
|
+
// OBO-capable configurations because both flows use the same credentials.
|
|
104
|
+
if (spiType === 'azure' && !tokenConfig.authType) {
|
|
105
|
+
tokenConfig.authType = 'azureserviceprincipal';
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
vendor: spiType,
|
|
109
|
+
modulePath,
|
|
110
|
+
tokenConfig,
|
|
111
|
+
cacheOptions: params.cacheOptions ? { ...params.cacheOptions } : undefined
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a provider from the `endUserSecParams` shape. Configuration fields
|
|
117
|
+
* that describe the identity provider are static. `endUserToken`,
|
|
118
|
+
* `endUserName`, `key`, `dataRoles`, and `attributes` are defaults only: request
|
|
119
|
+
* metadata supplied by runWithContext() wins.
|
|
120
|
+
*/
|
|
121
|
+
function configureUnifiedContextProvider(params) {
|
|
122
|
+
ensureInvocationHook();
|
|
123
|
+
const config = normalizeConfig(params);
|
|
124
|
+
const provider = loadProviderModule(config.provider);
|
|
125
|
+
return createProviderFn(config, provider);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeMetadata(defaults, current) {
|
|
129
|
+
if (!current || typeof current !== 'object') {
|
|
130
|
+
return defaults;
|
|
131
|
+
}
|
|
132
|
+
return { ...defaults, ...current };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getEndUserToken(metadata) {
|
|
136
|
+
return metadata.endUserToken;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createProviderFn(config, provider) {
|
|
140
|
+
const unifiedProvider = async function unifiedProvider() {
|
|
141
|
+
const current = securityContextProvider.getCurrentContext();
|
|
142
|
+
if (!current || typeof current !== "object") {
|
|
143
|
+
// Do not acquire an application token for normal work outside a scope.
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (hasOwn(current, 'authorization')) {
|
|
147
|
+
throwErr('Security context metadata.authorization is not supported; use endUserToken.');
|
|
148
|
+
}
|
|
149
|
+
const metadata = mergeMetadata(config.defaultMetadata, current);
|
|
150
|
+
const endUserToken = getEndUserToken(metadata);
|
|
151
|
+
// authFlow is fixed when the pool or standalone connection is created.
|
|
152
|
+
// Per-request metadata supplies identity only; it cannot select a token
|
|
153
|
+
// exchange for which the configured provider may lack credentials.
|
|
154
|
+
if (config.authFlow === "obo") {
|
|
155
|
+
if (config.provider.vendor !== 'azure') {
|
|
156
|
+
throwErr('On-behalf-of security contexts are supported only with spiType "azure".');
|
|
157
|
+
}
|
|
158
|
+
if (!endUserToken) {
|
|
159
|
+
throwErr("authFlow=obo requires an end-user token.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const tokenResult = await acquireOboToken(config, provider, endUserToken);
|
|
163
|
+
return getOrCreateScopedSecurityContext(unifiedProvider, config,
|
|
164
|
+
tokenResult, () => createSecurityContext(finalizeOboMode(metadata,
|
|
165
|
+
tokenResult.accessToken)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const tokenResult = await acquireAppToken(config, provider);
|
|
169
|
+
const databaseAccessToken = tokenResult.accessToken;
|
|
170
|
+
// No end-user identity means the database resolves the application
|
|
171
|
+
// identity directly from the client-credential database access token.
|
|
172
|
+
if (!endUserToken && !metadata.endUserName) {
|
|
173
|
+
// A key identifies a named database-managed end user. It is not valid
|
|
174
|
+
// for direct application logon, but roles and attributes are valid
|
|
175
|
+
// authorization metadata for either identity form.
|
|
176
|
+
if (metadata.key) {
|
|
177
|
+
throwErr("authFlow 'app' requires endUserName when key is supplied.");
|
|
178
|
+
}
|
|
179
|
+
return getOrCreateScopedSecurityContext(unifiedProvider, config,
|
|
180
|
+
tokenResult, () => createSecurityContext(
|
|
181
|
+
buildContext({
|
|
182
|
+
databaseAccessToken,
|
|
183
|
+
dataRoles: metadata.dataRoles,
|
|
184
|
+
attributes: metadata.attributes
|
|
185
|
+
})));
|
|
186
|
+
}
|
|
187
|
+
// App mode uses the application token to authorize database access and
|
|
188
|
+
// carries a supplied end-user token directly in the EUSC. This is not an
|
|
189
|
+
// OBO exchange; Azure and OCI both support this explicit identity form.
|
|
190
|
+
if (endUserToken) {
|
|
191
|
+
if (metadata.endUserName || metadata.key) {
|
|
192
|
+
throwErr('App mode cannot combine endUserToken with endUserName or key.');
|
|
193
|
+
}
|
|
194
|
+
return getOrCreateScopedSecurityContext(unifiedProvider, config,
|
|
195
|
+
tokenResult, () => createSecurityContext(finalizeAppTokenMode(metadata,
|
|
196
|
+
databaseAccessToken)));
|
|
197
|
+
}
|
|
198
|
+
if (!metadata.endUserName) {
|
|
199
|
+
throwErr("authFlow 'app' requires endUserName when endUserToken is supplied.");
|
|
200
|
+
}
|
|
201
|
+
return getOrCreateScopedSecurityContext(unifiedProvider, config,
|
|
202
|
+
tokenResult, () => createSecurityContext(
|
|
203
|
+
finalizeAppMode(metadata, databaseAccessToken)));
|
|
204
|
+
};
|
|
205
|
+
// The driver reads this private property to choose the provider lifecycle.
|
|
206
|
+
unifiedProvider._contextResolution = config.contextResolution;
|
|
207
|
+
return unifiedProvider;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function createSecurityContext(context) {
|
|
211
|
+
return new oracledb.EndUserSecurityContext(context);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Operation resolution calls the provider for every database operation so the
|
|
215
|
+
// token extension remains authoritative for expiry and refresh. When it
|
|
216
|
+
// confirms the same database token came from its cache, reuse the already
|
|
217
|
+
// encoded EUSC for this runWithContext() scope. The scope cache holds no
|
|
218
|
+
// plaintext token and is released when the async scope completes.
|
|
219
|
+
function getOrCreateScopedSecurityContext(provider, config, tokenResult,
|
|
220
|
+
createContext) {
|
|
221
|
+
if (config.contextResolution !== 'operation') {
|
|
222
|
+
return createContext();
|
|
223
|
+
}
|
|
224
|
+
const cached = securityContextProvider._getScopedValue(provider);
|
|
225
|
+
if (cached && tokenResult.isNewToken === false) {
|
|
226
|
+
return cached;
|
|
227
|
+
}
|
|
228
|
+
const securityContext = createContext();
|
|
229
|
+
securityContextProvider._setScopedValue(provider, securityContext);
|
|
230
|
+
return securityContext;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function finalizeOboMode(metadata, databaseAccessToken) {
|
|
234
|
+
const attributeResolution = resolveAttributesForRequest(metadata);
|
|
235
|
+
const context = {
|
|
236
|
+
databaseAccessToken,
|
|
237
|
+
dataRoles: metadata.dataRoles,
|
|
238
|
+
attributes: attributeResolution.payload
|
|
239
|
+
};
|
|
240
|
+
context.endUserToken = getEndUserToken(metadata);
|
|
241
|
+
return buildContext(context);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function finalizeAppMode(metadata, databaseAccessToken) {
|
|
245
|
+
const context = buildContext({
|
|
246
|
+
databaseAccessToken,
|
|
247
|
+
endUserName: metadata.endUserName,
|
|
248
|
+
dataRoles: metadata.dataRoles,
|
|
249
|
+
key: metadata.key,
|
|
250
|
+
attributes: metadata.attributes
|
|
251
|
+
});
|
|
252
|
+
return context;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function finalizeAppTokenMode(metadata, databaseAccessToken) {
|
|
256
|
+
return buildContext({
|
|
257
|
+
databaseAccessToken,
|
|
258
|
+
endUserToken: metadata.endUserToken,
|
|
259
|
+
dataRoles: metadata.dataRoles,
|
|
260
|
+
attributes: metadata.attributes
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function acquireAppToken(config, provider) {
|
|
265
|
+
const result = await provider.getTokenResult(config.provider.tokenConfig);
|
|
266
|
+
return {
|
|
267
|
+
accessToken: getAccessToken(result,
|
|
268
|
+
'Token provider failed to return an application access token.'),
|
|
269
|
+
isNewToken: result?.isNewToken !== false,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function acquireOboToken(config, provider, endUserToken) {
|
|
274
|
+
const result = await provider.getOnBehalfOfToken({
|
|
275
|
+
endUserToken,
|
|
276
|
+
oboConfig: config.provider.tokenConfig,
|
|
277
|
+
cacheOptions: config.provider.cacheOptions
|
|
278
|
+
});
|
|
279
|
+
const accessToken = getAccessToken(result,
|
|
280
|
+
'Token provider failed to return an access token for on-behalf-of exchange.');
|
|
281
|
+
return {
|
|
282
|
+
accessToken,
|
|
283
|
+
// Extensions set false only when returning a valid cached database token.
|
|
284
|
+
// Unknown/custom provider results are conservative and force a new EUSC.
|
|
285
|
+
isNewToken: result?.isNewToken !== false,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getAccessToken(result, errorMessage) {
|
|
290
|
+
const accessToken = typeof result === 'string'
|
|
291
|
+
? result
|
|
292
|
+
: (result?.accessToken ?? result?.token);
|
|
293
|
+
if (typeof accessToken !== 'string' || !accessToken.trim()) {
|
|
294
|
+
throwErr(errorMessage);
|
|
295
|
+
}
|
|
296
|
+
return accessToken;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function loadProviderModule(descriptor) {
|
|
300
|
+
return require(descriptor.modulePath);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildContext(base) {
|
|
304
|
+
const context = { databaseAccessToken: base.databaseAccessToken };
|
|
305
|
+
for (const key of ['endUserToken', 'endUserName', 'dataRoles', 'key', 'attributes']) {
|
|
306
|
+
if (base[key] !== undefined) {
|
|
307
|
+
context[key] = base[key];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return context;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeConfig(params) {
|
|
314
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
315
|
+
throw new TypeError('endUserSecParams must be an object.');
|
|
316
|
+
}
|
|
317
|
+
const defaultMetadata = {};
|
|
318
|
+
for (const key of DEFAULT_METADATA_FIELDS) {
|
|
319
|
+
if (hasOwn(params, key)) {
|
|
320
|
+
defaultMetadata[key] = params[key];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const authFlow = normalizeAuthFlow(params.authFlow);
|
|
324
|
+
const provider = normalizeProvider(params);
|
|
325
|
+
if (authFlow === 'obo' && provider.vendor !== 'azure') {
|
|
326
|
+
throwErr('On-behalf-of security contexts are supported only with spiType "azure".');
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
authFlow,
|
|
330
|
+
contextResolution: normalizeContextResolution(params.contextResolution),
|
|
331
|
+
defaultMetadata: snapshotDefaultMetadata(defaultMetadata),
|
|
332
|
+
provider
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Provider defaults are static after pool/connection creation. Snapshot the
|
|
337
|
+
// mutable fields so changes to the caller's configuration object cannot alter
|
|
338
|
+
// a scope after the provider has been configured.
|
|
339
|
+
function snapshotDefaultMetadata(metadata) {
|
|
340
|
+
const snapshot = { ...metadata };
|
|
341
|
+
if (Array.isArray(metadata.dataRoles)) {
|
|
342
|
+
snapshot.dataRoles = [...metadata.dataRoles];
|
|
343
|
+
}
|
|
344
|
+
if (metadata.attributes && typeof metadata.attributes === 'object') {
|
|
345
|
+
snapshot.attributes = JSON.parse(JSON.stringify(metadata.attributes));
|
|
346
|
+
}
|
|
347
|
+
return snapshot;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolveAttributesForRequest(metadata) {
|
|
351
|
+
if (!hasOwn(metadata, 'attributes')) {
|
|
352
|
+
return { payload: undefined };
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
JSON.stringify(metadata.attributes);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const error = new Error('security context attributes must be JSON-serializable.');
|
|
358
|
+
error.cause = err;
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
return { payload: metadata.attributes };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Process configuration hook. `endUserSecParams` is copied to the pool or
|
|
366
|
+
* standalone connection being created. Dynamic request metadata is supplied
|
|
367
|
+
* later with runWithContext().
|
|
368
|
+
*/
|
|
369
|
+
function hookFn(options) {
|
|
370
|
+
if (!options.endUserSecParams) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
options._securityContextProvider =
|
|
374
|
+
configureUnifiedContextProvider(options.endUserSecParams);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
oracledb.registerProcessConfigurationHook(hookFn);
|
|
@@ -26,9 +26,27 @@
|
|
|
26
26
|
'use strict';
|
|
27
27
|
const oracledb = require('oracledb');
|
|
28
28
|
const msal = require('@azure/msal-node');
|
|
29
|
+
const { TokenCache, hashValue } = require('../cache.js');
|
|
29
30
|
|
|
31
|
+
const OBO_DEFAULT_CLOCK_SKEW_MS = 60 * 1000;
|
|
32
|
+
const TOKEN_CACHE_MAX_ENTRIES = 100;
|
|
33
|
+
// Process-wide, bounded LRU cache of completed OBO exchanges. Connections
|
|
34
|
+
// safely share entries only when their cache key has the same caller-token
|
|
35
|
+
// hash and every Azure credential/request input that can change the result.
|
|
36
|
+
// Values are obfuscated at rest.
|
|
37
|
+
const oboTokenCache = new TokenCache(TOKEN_CACHE_MAX_ENTRIES);
|
|
38
|
+
// Process-wide, bounded cache of service-principal tokens. Its key includes
|
|
39
|
+
// every credential and request input that can change the resulting token, so
|
|
40
|
+
// pools can share only compatible entries. Values are obfuscated at rest.
|
|
41
|
+
const appTokenCache = new TokenCache(TOKEN_CACHE_MAX_ENTRIES);
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
|
|
44
|
+
// Internal Deep Security API. The driver's ordinary token-authentication hook
|
|
45
|
+
// below returns this result's access-token string.
|
|
46
|
+
async function getTokenResult(params) {
|
|
47
|
+
if (typeof params?.authType !== 'string') {
|
|
48
|
+
throwErr('Azure authentication type is required.');
|
|
49
|
+
}
|
|
32
50
|
switch (params.authType.toLowerCase()) {
|
|
33
51
|
case 'azureserviceprincipal':
|
|
34
52
|
return await servicePrincipalCredentials(params);
|
|
@@ -46,9 +64,7 @@ function throwErr(message) {
|
|
|
46
64
|
|
|
47
65
|
//---------------------------------------------------------------------------
|
|
48
66
|
// Returns the access token for authentication as a service principal. This
|
|
49
|
-
// authentication method requires a client ID
|
|
50
|
-
// certificate. These values may be provided as config parameters read by
|
|
51
|
-
// the Azure SDK.
|
|
67
|
+
// authentication method requires a client ID and client secret.
|
|
52
68
|
// ---------------------------------------------------------------------------
|
|
53
69
|
async function servicePrincipalCredentials(params) {
|
|
54
70
|
const clientId = params.clientId ??
|
|
@@ -57,31 +73,24 @@ async function servicePrincipalCredentials(params) {
|
|
|
57
73
|
throwErr("Token based authentication config parameter authority is missing for azureServicePrincipal in extensionAzure plugins.");
|
|
58
74
|
const clientSecret = params.clientSecret ??
|
|
59
75
|
throwErr("Token based authentication config parameter clientSecret is missing for azureServicePrincipal in extensionAzure plugins.");
|
|
60
|
-
const msalConfig = {
|
|
61
|
-
auth: {
|
|
62
|
-
clientId: clientId,
|
|
63
|
-
authority: authority,
|
|
64
|
-
clientSecret: clientSecret,
|
|
65
|
-
},
|
|
66
|
-
system: {
|
|
67
|
-
// Use it while generating azure tokens behind proxy
|
|
68
|
-
// Otherwise can remove this option
|
|
69
|
-
networkClient: params.proxy,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
if (!msalConfig.system.networkClient) {
|
|
74
|
-
delete msalConfig.system.networkClient;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
76
|
const scopes = params.scopes ??
|
|
78
77
|
throwErr("Token based authentication config parameter scopes is missing for azureServicePrincipal in extensionAzure plugins.");
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
const normalizedScopes = normalizeScopes(scopes);
|
|
79
|
+
const config = { ...params, clientId, authority, clientSecret };
|
|
80
|
+
const cacheKey = computeAppCacheKey(config, normalizedScopes);
|
|
81
|
+
const result = await appTokenCache.get(cacheKey, async () => {
|
|
82
|
+
const authResponse = await createClient(config).acquireTokenByClientCredential({
|
|
83
|
+
scopes: normalizedScopes,
|
|
84
|
+
});
|
|
85
|
+
if (!authResponse?.accessToken) {
|
|
86
|
+
throwErr('Failed to acquire application access token from Azure.');
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
value: authResponse.accessToken,
|
|
90
|
+
validUntil: calculateExpiry(authResponse, OBO_DEFAULT_CLOCK_SKEW_MS),
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
return { accessToken: result.value, isNewToken: !result.isCacheHit };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
// ---------------------------------------------------------------------------
|
|
@@ -92,9 +101,132 @@ async function servicePrincipalCredentials(params) {
|
|
|
92
101
|
function hookFn(options) {
|
|
93
102
|
if (options.tokenAuthConfigAzure) {
|
|
94
103
|
options.accessToken = async function callbackFn(refresh, config) {
|
|
95
|
-
return await
|
|
104
|
+
return (await getTokenResult(config)).accessToken;
|
|
96
105
|
};
|
|
97
106
|
options.accessTokenConfig = options.tokenAuthConfigAzure;
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
109
|
oracledb.registerProcessConfigurationHook(hookFn);
|
|
110
|
+
|
|
111
|
+
//---------------------------------------------------------------------------
|
|
112
|
+
// normalizeScopes()
|
|
113
|
+
//---------------------------------------------------------------------------
|
|
114
|
+
function normalizeScopes(scopes) {
|
|
115
|
+
const normalized = Array.isArray(scopes)
|
|
116
|
+
? scopes
|
|
117
|
+
: (typeof scopes === 'string' ? [scopes] : null);
|
|
118
|
+
if (!normalized || normalized.some((scope) =>
|
|
119
|
+
typeof scope !== 'string' || !scope.trim())) {
|
|
120
|
+
throwErr('Azure scopes must be a string or an array of non-empty strings.');
|
|
121
|
+
}
|
|
122
|
+
return normalized.map((scope) => scope.trim()).sort();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//---------------------------------------------------------------------------
|
|
126
|
+
// buildNetworkClient()
|
|
127
|
+
//---------------------------------------------------------------------------
|
|
128
|
+
function buildNetworkClient(proxyConfig) {
|
|
129
|
+
if (!proxyConfig) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (typeof proxyConfig.sendGetRequestAsync === 'function' &&
|
|
133
|
+
typeof proxyConfig.sendPostRequestAsync === 'function') {
|
|
134
|
+
return proxyConfig;
|
|
135
|
+
}
|
|
136
|
+
throwErr('Azure proxy configuration must implement sendGetRequestAsync() and sendPostRequestAsync().');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//---------------------------------------------------------------------------
|
|
140
|
+
// computeOboCacheKey()
|
|
141
|
+
//---------------------------------------------------------------------------
|
|
142
|
+
function computeOboCacheKey(config, scopes, endUserToken) {
|
|
143
|
+
// Do not put the assertion or client secret itself in a process-wide Map.
|
|
144
|
+
// The secret hash separates entries if a client secret is rotated while the
|
|
145
|
+
// process is running.
|
|
146
|
+
return JSON.stringify([
|
|
147
|
+
config.authType?.toLowerCase(), config.clientId,
|
|
148
|
+
hashValue(config.clientSecret), config.authority, scopes,
|
|
149
|
+
hashValue(endUserToken),
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function computeAppCacheKey(config, scopes) {
|
|
154
|
+
// Hash the secret rather than retaining it in the process-wide key. A
|
|
155
|
+
// changed secret creates a new entry, leaving the old one bounded and
|
|
156
|
+
// eligible for LRU eviction.
|
|
157
|
+
return JSON.stringify([
|
|
158
|
+
config.authType?.toLowerCase(), config.clientId,
|
|
159
|
+
hashValue(config.clientSecret), config.authority, scopes,
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createClient(config) {
|
|
164
|
+
const networkClient = buildNetworkClient(config.proxy);
|
|
165
|
+
const msalConfig = {
|
|
166
|
+
auth: {
|
|
167
|
+
clientId: config.clientId ?? throwErr("OBO configuration requires clientId."),
|
|
168
|
+
authority: config.authority ?? throwErr("OBO configuration requires authority."),
|
|
169
|
+
clientSecret: config.clientSecret ?? throwErr("OBO configuration requires clientSecret."),
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
if (networkClient) {
|
|
173
|
+
msalConfig.system = { networkClient };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return new msal.ConfidentialClientApplication(msalConfig);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//---------------------------------------------------------------------------
|
|
180
|
+
// calculateExpiry()
|
|
181
|
+
//---------------------------------------------------------------------------
|
|
182
|
+
function calculateExpiry(result, clockSkewMs) {
|
|
183
|
+
if (result?.expiresOn instanceof Date) {
|
|
184
|
+
return result.expiresOn.getTime() - clockSkewMs;
|
|
185
|
+
}
|
|
186
|
+
throwErr('Azure token response did not include an expiration time.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
//---------------------------------------------------------------------------
|
|
190
|
+
// getOnBehalfOfToken()
|
|
191
|
+
//---------------------------------------------------------------------------
|
|
192
|
+
async function getOnBehalfOfToken({ endUserToken, oboConfig, cacheOptions }) {
|
|
193
|
+
if (!endUserToken) {
|
|
194
|
+
throwErr('An end user access token must be supplied for OBO exchange.');
|
|
195
|
+
}
|
|
196
|
+
if (!oboConfig) {
|
|
197
|
+
throwErr('OBO configuration is required.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const scopes = normalizeScopes(oboConfig.scopes);
|
|
201
|
+
const cacheEnabled = cacheOptions?.enabled !== false;
|
|
202
|
+
const clockSkewMs = cacheOptions?.clockSkewMs ?? OBO_DEFAULT_CLOCK_SKEW_MS;
|
|
203
|
+
// This process-wide bounded cache is authoritative. Do not use MSAL's
|
|
204
|
+
// per-client cache because clients are intentionally short-lived so they do
|
|
205
|
+
// not retain credentials in a long-lived object graph.
|
|
206
|
+
|
|
207
|
+
const cacheKey = computeOboCacheKey(oboConfig, scopes, endUserToken);
|
|
208
|
+
const result = await oboTokenCache.get(cacheKey, async () => {
|
|
209
|
+
const authResponse = await createClient(oboConfig).acquireTokenOnBehalfOf({
|
|
210
|
+
oboAssertion: endUserToken,
|
|
211
|
+
scopes,
|
|
212
|
+
skipCache: true
|
|
213
|
+
});
|
|
214
|
+
if (!authResponse?.accessToken) {
|
|
215
|
+
throwErr('Failed to acquire on-behalf-of access token from Azure.');
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
value: authResponse.accessToken,
|
|
219
|
+
validUntil: calculateExpiry(authResponse, clockSkewMs),
|
|
220
|
+
};
|
|
221
|
+
}, { enabled: cacheEnabled });
|
|
222
|
+
return {
|
|
223
|
+
accessToken: result.value,
|
|
224
|
+
expiresOn: new Date(result.validUntil + clockSkewMs),
|
|
225
|
+
isNewToken: !result.isCacheHit,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
getTokenResult,
|
|
231
|
+
getOnBehalfOfToken
|
|
232
|
+
};
|