From 1ac7b870c9fd1bd7ad5adee7d61161b3c9d6d4ca Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Fri, 22 May 2026 13:48:43 +0200 Subject: [PATCH 1/7] HCK-16220: lowercase TNS data keys in parseTns parser Co-authored-by: Cursor --- reverse_engineering/helpers/parseTns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reverse_engineering/helpers/parseTns.js b/reverse_engineering/helpers/parseTns.js index 0cd5f90c..8850ef69 100644 --- a/reverse_engineering/helpers/parseTns.js +++ b/reverse_engineering/helpers/parseTns.js @@ -19,7 +19,7 @@ function parseObject(lex, obj = {}) { return { ...obj, - [id]: value, + [id.toLowerCase()]: value, }; } From ff3ccfa89e7cbc6d0f1d66fc64c3385732ab9b5a Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Fri, 22 May 2026 16:08:34 +0200 Subject: [PATCH 2/7] HCK-16220: fix TNS/mTLS connections and improve RE error handling. Gate wallet usage on explicit mTLS while preserving legacy TNS profiles, normalize connect strings, pre-check DNS for unusable Azure hostnames, and propagate connection failures instead of empty schema lists. Co-authored-by: Cursor --- reverse_engineering/api.js | 25 +- .../connectionSettingsModalConfig.json | 81 +++- reverse_engineering/helpers/extractWallet.js | 1 + reverse_engineering/helpers/oracleHelper.js | 399 +++++++++++++++--- 4 files changed, 423 insertions(+), 83 deletions(-) diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index c4b5af49..541f9524 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -29,12 +29,29 @@ module.exports = { }, async testConnection(connectionInfo, logger, callback, app) { + const sshService = app.require('@hackolade/ssh-service'); + try { - await this.connect(connectionInfo, logger, () => {}, app); + logInfo('Test connection', connectionInfo, logger); + oracleHelper.logEnvironment(logger); + await oracleHelper.disconnect(sshService); + await oracleHelper.connect(connectionInfo, sshService, message => { + logger.log('info', message, 'Connection'); + }); callback(null); } catch (error) { logger.log('error', { message: error.message, stack: error.stack, error }, 'Test connection'); callback({ message: error.message, stack: error.stack }); + } finally { + try { + await oracleHelper.disconnect(sshService); + } catch (disconnectError) { + logger.log( + 'warn', + { message: disconnectError.message, stack: disconnectError.stack }, + 'Disconnect after test connection', + ); + } } }, @@ -42,7 +59,11 @@ module.exports = { try { logInfo('Get schemas', connectionInfo, logger); await this.connect(connectionInfo, logger, () => {}, app); - const schemas = await oracleHelper.getSchemaNames(); + const schemas = await oracleHelper.getSchemaNames(connectionInfo, { + info: data => logger.log('info', data, 'Get schemas'), + error: error => + logger.log('error', { message: error.message, stack: error.stack, error }, 'Get schemas'), + }); logger.log('info', schemas, 'All schemas list', connectionInfo.hiddenKeys); return callback(null, schemas); } catch (error) { diff --git a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json index 04e18d9f..845398dc 100644 --- a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json +++ b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json @@ -99,7 +99,7 @@ } }, { - "inputLabel": "Wallet file", + "inputLabel": "Wallet archive", "inputKeyword": "walletFile", "description": "Specify the full path location and name of the wallet zip file", "inputType": "file", @@ -110,11 +110,23 @@ } }, { - "inputLabel": "Tnsnames Directory", + "inputLabel": "Tnsnames file", "inputKeyword": "TNSpath", - "description": "Specify the full path to directory with tnsnames.ora file", + "description": "Specify the full path to tnsnames.ora.", "inputType": "file", "openType": "openDirectory", + "extensions": ["ora", "txt"], + "dependency": { + "key": "connectionMethod", + "value": ["TNS"] + } + }, + { + "inputLabel": "Mutual TLS (mTLS)", + "inputKeyword": "mutualTLS", + "description": "Enable only when the database requires a client wallet (mutual TLS). Leave unchecked for legacy TNS connections that use a tnsnames.ora directory with username/password only.", + "inputType": "checkbox", + "defaultValue": false, "dependency": { "key": "connectionMethod", "value": ["TNS"] @@ -139,25 +151,17 @@ "inputKeyword": "serviceName", "description": "Specify the service name of the Oracle Instance", "inputType": "text", + "regex": "([^\\s])", "dependency": { - "type": "or", + "type": "and", "values": [ { "key": "connectionMethod", - "value": ["TNS"] + "value": ["Basic"] }, { - "type": "and", - "values": [ - { - "key": "connectionMethod", - "value": ["Basic"] - }, - { - "key": "identifierType", - "value": ["serviceName"] - } - ] + "key": "identifierType", + "value": ["serviceName"] } ] } @@ -165,23 +169,56 @@ { "inputLabel": "Wallet Password", "inputKeyword": "walletPassword", - "description": "Specify the password used to protect the wallet", + "description": "OCI wallet password (not the database user password). Required for Cloud Wallet and TNS with mTLS enabled.", "inputType": "password", "isHiddenKey": true, "dependency": { - "type": "and", + "type": "or", "values": [ { - "key": "connectionMethod", - "value": ["Wallet"] + "type": "and", + "values": [ + { + "key": "connectionMethod", + "value": ["Wallet"] + }, + { + "key": "mode", + "value": ["thin"] + } + ] }, { - "key": "mode", - "value": ["thin"] + "type": "and", + "values": [ + { + "key": "connectionMethod", + "value": ["TNS"] + }, + { + "key": "mode", + "value": ["thin"] + }, + { + "key": "mutualTLS", + "value": [true, "true"] + } + ] } ] } }, + { + "inputLabel": "TNS alias", + "inputKeyword": "serviceName", + "description": "Optional. Leave empty to use the first entry in tnsnames.ora (e.g. _high).", + "inputType": "text", + "inputPlaceholder": "Optional TNS alias", + "dependency": { + "key": "connectionMethod", + "value": ["TNS"] + } + }, { "inputLabel": "SID", "inputKeyword": "sid", diff --git a/reverse_engineering/helpers/extractWallet.js b/reverse_engineering/helpers/extractWallet.js index d3ae40eb..14a1ff46 100644 --- a/reverse_engineering/helpers/extractWallet.js +++ b/reverse_engineering/helpers/extractWallet.js @@ -92,3 +92,4 @@ const extractWallet = async ({ walletFile, tempFolder, name }) => { }; module.exports = extractWallet; +module.exports.fixSqlNetOraWalletPath = replaceSqlNetOraDirectoryPath; diff --git a/reverse_engineering/helpers/oracleHelper.js b/reverse_engineering/helpers/oracleHelper.js index 510d1bb4..a08e26c4 100644 --- a/reverse_engineering/helpers/oracleHelper.js +++ b/reverse_engineering/helpers/oracleHelper.js @@ -1,8 +1,12 @@ const _ = require('lodash'); +const dns = require('dns'); +const dnsPromises = dns.promises; const fs = require('fs'); +const net = require('net'); const path = require('path'); const oracleDB = require('oracledb'); const extractWallet = require('./extractWallet'); +const fixSqlNetOraWalletPath = require('./extractWallet').fixSqlNetOraWalletPath; const parseTns = require('./parseTns'); const { getSchemaSequences } = require('./getSchemaSequences'); const { getSchemaSynonyms } = require('./getSchemaSynonyms'); @@ -11,6 +15,20 @@ const noConnectionError = { message: 'Connection error' }; let connection; let useSshTunnel; +let pluginTnsAdmin; + +const setPluginTnsAdmin = configDir => { + pluginTnsAdmin = configDir; + process.env.TNS_ADMIN = configDir; +}; + +const clearPluginTnsAdmin = () => { + if (pluginTnsAdmin && process.env.TNS_ADMIN === pluginTnsAdmin) { + delete process.env.TNS_ADMIN; + } + + pluginTnsAdmin = null; +}; const parseProxyOptions = (proxyString = '') => { const result = proxyString.match(/http:\/\/(?:.*?:.*?@)?(.*?):(\d+)/i); @@ -25,9 +43,44 @@ const parseProxyOptions = (proxyString = '') => { }; }; +const TNS_NAMES_FILE = 'tnsnames.ora'; + +const resolveTnsConfigDir = tnsPath => { + if (!tnsPath) { + return tnsPath; + } + + const normalizedPath = path.normalize(String(tnsPath).trim()); + + if (!fs.existsSync(normalizedPath)) { + return normalizedPath; + } + + if (fs.statSync(normalizedPath).isDirectory()) { + return normalizedPath; + } + + if (path.basename(normalizedPath).toLowerCase() === TNS_NAMES_FILE) { + return path.dirname(normalizedPath); + } + + throw new Error(`Invalid TNS path "${normalizedPath}". Select the wallet directory or the ${TNS_NAMES_FILE} file.`); +}; + +const assertTnsConfigDir = configDir => { + const tnsNamesOraFile = getTnsNamesOraFile(configDir); + + if (!tnsNamesOraFile || !fs.existsSync(tnsNamesOraFile)) { + throw new Error( + `Cannot find ${TNS_NAMES_FILE} in "${configDir}". Select the wallet directory or the ${TNS_NAMES_FILE} file.`, + ); + } +}; + const getTnsNamesOraFile = configDir => { + const resolvedConfigDir = resolveTnsConfigDir(configDir); const tnsNamesOraFile = [ - configDir, + resolvedConfigDir, process.env.TNS_ADMIN, path.join(process.env.ORACLE_HOME || '', 'network', 'admin'), path.join(process.env.LD_LIBRARY_PATH || '', 'network', 'admin'), @@ -54,11 +107,62 @@ const parseTnsNamesOra = filePath => { return result; }; -const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger) => { +const WALLET_FILES = ['ewallet.pem', 'cwallet.sso', 'ewallet.p12']; +const MTLS_PORT = '1522'; + +const hasWalletFiles = configDir => + configDir && fs.existsSync(configDir) && WALLET_FILES.some(file => fs.existsSync(path.join(configDir, file))); + +const hasAutoLoginWallet = configDir => configDir && fs.existsSync(path.join(configDir, 'cwallet.sso')); + +const isMutualTlsEnabled = mutualTLS => mutualTLS === true || mutualTLS === 'true'; + +const assertTnsMtlsRequirements = ({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }) => { + if (!useMutualTls) { + if (isMtlsPort(tnsServicePort)) { + logger({ + message: `TNS service uses port ${MTLS_PORT} without mutual TLS enabled. Connecting in legacy TNS mode (server TLS only, no wallet). Enable "Mutual TLS (mTLS)" if the database requires a client wallet.`, + }); + } + + return; + } + + if (!isMtlsPort(tnsServicePort)) { + return; + } + + if (!hasWalletFiles(configDir)) { + throw new Error( + `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, + ); + } + + if (!walletPassword && !hasAutoLoginWallet(configDir)) { + throw new Error( + `Mutual TLS requires a wallet password (OCI wallet zip password), unless the directory contains an auto-login wallet (cwallet.sso).`, + ); + } + + if (!walletPassword && hasAutoLoginWallet(configDir)) { + logger({ + message: 'Using auto-login wallet (cwallet.sso); wallet password not required.', + }); + } +}; + +const isMtlsPort = port => String(port) === MTLS_PORT; + +const connectStringUsesMtlsPort = connectString => /\(PORT\s*=\s*1522\)/i.test(connectString); + +const normalizeTnsAlias = serviceName => (serviceName == null ? '' : String(serviceName).trim()); + +const getResolvedTnsService = (configDir, serviceName, logger) => { + const tnsAlias = normalizeTnsAlias(serviceName); const filePath = getTnsNamesOraFile(configDir); if (!fs.existsSync(filePath)) { - return serviceName; + return null; } logger({ message: 'Found tnsnames.ora file: ' + filePath }); @@ -68,28 +172,80 @@ const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger) => logger({ message: 'tnsnames.ora successfully parsed' }); const tnsServicesNames = Object.keys(tnsData); - if (!tnsData[serviceName] && tnsServicesNames.length === 0) { - logger({ message: `Cannot find '${serviceName}' in tnsnames.ora and no fallback found` }); - return serviceName; + if (tnsServicesNames.length === 0) { + logger({ message: 'No TNS services found in tnsnames.ora' }); + return null; } const [firstTnsServiceName] = tnsServicesNames; - const tnsService = tnsData[serviceName] || tnsData[firstTnsServiceName]; - if (!tnsData[serviceName]) { + const tnsService = (tnsAlias && tnsData[tnsAlias]) || tnsData[firstTnsServiceName]; + + if (!tnsAlias) { logger({ - message: `Connect using first TNS service ${firstTnsServiceName}' from ${path.join(configDir, 'tnsnames.ora')}.`, + message: `No TNS alias provided. Using first TNS service ${firstTnsServiceName} from ${path.join(configDir, 'tnsnames.ora')}.`, + }); + } else if (!tnsData[tnsAlias]) { + logger({ + message: `TNS alias '${tnsAlias}' not found. Using first TNS service ${firstTnsServiceName} from ${path.join(configDir, 'tnsnames.ora')}.`, }); } else { logger({ - message: `Connect using TNS service ${serviceName}' from ${path.join(configDir, 'tnsnames.ora')}.`, + message: `Connect using TNS service ${tnsAlias} from ${path.join(configDir, 'tnsnames.ora')}.`, }); } - const address = tnsService?.data?.description?.address; - const service = tnsService?.data?.description?.connect_data?.service_name; - const sid = tnsService?.data?.description?.connect_data?.sid; + const description = tnsService?.data?.description; + const address = description?.address; + const resolvedAlias = tnsAlias && tnsData[tnsAlias] ? tnsAlias : firstTnsServiceName; + + return { + description, + address, + service: description?.connect_data?.service_name, + sid: description?.connect_data?.sid, + port: address?.port, + resolvedAlias, + }; +}; + +const syncConnectionEndpointFromTns = (connectionInfo, configDir, serviceName, logger) => { + const resolved = getResolvedTnsService(configDir, serviceName, logger); + + if (!resolved?.address?.host) { + return resolved; + } + + connectionInfo.host = resolved.address.host; + connectionInfo.port = resolved.address.port; + + logger({ + message: 'Synced connection host/port from tnsnames.ora for connections list', + host: connectionInfo.host, + port: connectionInfo.port, + tnsAlias: resolved.resolvedAlias, + }); + + return resolved; +}; + +const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger, useWallet = false) => { + const resolved = getResolvedTnsService(configDir, serviceName, logger); + + if (!resolved) { + return serviceName; + } + + const { description, address, service, sid, port, resolvedAlias } = resolved; - logger({ message: 'tnsnames.ora', address, service }); + logger({ message: 'tnsnames.ora', address, service, port }); + + if (useWallet) { + logger({ + message: 'Using TNS alias with mTLS wallet', + connectString: resolvedAlias, + }); + return resolvedAlias; + } return getConnectionDescription( _.omitBy( @@ -98,7 +254,10 @@ const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger) => ...proxy, protocol: address?.protocol || 'tcps', service: service || serviceName, - sid: sid, + sid, + retryCount: description?.retry_count, + retryDelay: description?.retry_delay, + sslServerDnMatch: description?.security?.ssl_server_dn_match, }, _.isUndefined, ), @@ -108,8 +267,48 @@ const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger) => const combine = (val, str) => (val ? str : ''); -const getConnectionDescription = ({ protocol, host, port, sid, service, httpsProxy, httpsProxyPort }, logger) => { - const connectionString = `(DESCRIPTION= +const normalizeConnectString = connectString => + typeof connectString === 'string' ? connectString.replace(/\s+/g, '') : connectString; + +const UNUSABLE_RESOLVED_HOSTS = new Set(['255.255.255.255', '0.0.0.0']); + +const assertResolvableConnectHost = async (hostname, logger) => { + if (!hostname || net.isIP(hostname)) { + return; + } + + let addresses; + + try { + addresses = await dnsPromises.lookup(hostname, { all: true }); + } catch (error) { + throw new Error(`Cannot resolve hostname "${hostname}": ${error.message}`); + } + + const resolvedAddresses = addresses.map(entry => entry.address); + + logger({ + message: 'Resolved connection hostname for TCP connect', + hostname, + resolvedAddresses, + }); + + const unusableAddress = resolvedAddresses.find(address => UNUSABLE_RESOLVED_HOSTS.has(address)); + + if (unusableAddress) { + throw new Error( + `Hostname "${hostname}" resolves to ${unusableAddress}. This often means an Azure VM is stopped or its public IP was deallocated. Start the VM or update the hostname, then try again.`, + ); + } +}; + +const getConnectionDescription = ( + { protocol, host, port, sid, service, httpsProxy, httpsProxyPort, retryCount, retryDelay, sslServerDnMatch }, + logger, +) => { + const connectionString = normalizeConnectString(`(DESCRIPTION= + ${combine(retryCount, `(RETRY_COUNT=${retryCount})`)} + ${combine(retryDelay, `(RETRY_DELAY=${retryDelay})`)} (ADDRESS= (PROTOCOL=${protocol || 'tcp'}) (HOST=${host}) @@ -120,8 +319,9 @@ const getConnectionDescription = ({ protocol, host, port, sid, service, httpsPro ${combine(sid, `(SID=${sid})`)} ${combine(service, `(SERVICE_NAME=${service})`)} ) - )`; - logger({ message: 'connectionString', connectionString }); + ${combine(sslServerDnMatch, `(SECURITY=(SSL_SERVER_DN_MATCH=${sslServerDnMatch}))`)} + )`); + logger({ message: 'connectString', connectString: connectionString }); return connectionString; }; @@ -134,32 +334,22 @@ const getSshConnectionString = async (data, sshService, logger) => { }; if (['Wallet', 'TNS'].includes(data.connectionMethod)) { - const filePath = getTnsNamesOraFile(data.configDir); + const resolved = getResolvedTnsService(data.configDir, data.serviceName, logger); - if (!fs.existsSync(filePath)) { + if (!resolved) { throw new Error( 'Cannot find tnsnames.ora file. Please, specify tnsnames folder or use Base connection method.', ); } - logger({ message: 'Found tnsnames.ora file: ' + filePath }); - - const tnsData = parseTnsNamesOra(filePath); - - if (!tnsData[data.serviceName]) { - throw new Error('Cannot find "' + data.serviceName + '" in tnsnames.ora'); - } - - const address = tnsData[data.serviceName]?.data?.description?.address; - const service = tnsData[data.serviceName]?.data?.description?.connect_data?.service_name; - const sid = tnsData[data.serviceName]?.data?.description?.connect_data?.sid; + const { address, service, sid } = resolved; logger({ message: 'tnsnames.ora', address, service }); connectionData.protocol = address?.protocol; connectionData.host = address?.host; connectionData.port = address?.port; - connectionData.service = service || data.serviceName; + connectionData.service = service || normalizeTnsAlias(data.serviceName); connectionData.sid = sid; } else { connectionData.host = data.host; @@ -190,8 +380,8 @@ const getSshConnectionString = async (data, sshService, logger) => { ); }; -const connect = async ( - { +const connect = async (connectionInfo, sshService, logger) => { + const { walletFile, walletPassword, tempFolder, @@ -219,14 +409,33 @@ const connect = async ( ssh_password, authRole, mode, - }, - sshService, - logger, -) => { + mutualTLS, + } = connectionInfo; + + if (connectionMethod === 'TNS' && TNSpath) { + try { + const tnsConfigDir = resolveTnsConfigDir(TNSpath); + const tnsNamesOraFile = getTnsNamesOraFile(tnsConfigDir); + + if (tnsNamesOraFile && fs.existsSync(tnsNamesOraFile)) { + syncConnectionEndpointFromTns(connectionInfo, tnsConfigDir, serviceName, logger); + } + } catch (error) { + logger({ message: `Unable to sync host/port from tnsnames.ora: ${error.message}` }); + } + } + if (connection) { + logger({ message: 'Reusing existing Oracle connection' }); return connection; } + if (connectionMethod === 'Basic') { + clearPluginTnsAdmin(); + } + + const useMutualTls = isMutualTlsEnabled(mutualTLS); + const MODES = { thin: 'thin', thick: 'thick', @@ -235,14 +444,44 @@ const connect = async ( let libDir; let credentials = {}; let proxy = ''; + let tnsServicePort; + + if (connectionMethod === 'Basic' && !normalizeTnsAlias(serviceName)) { + throw new Error('Service name is required for Basic connection method.'); + } if (connectionMethod === 'Wallet') { configDir = await extractWallet({ walletFile, tempFolder, name }); - process.env.TNS_ADMIN = configDir; + setPluginTnsAdmin(configDir); + const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); + tnsServicePort = resolvedTnsService?.port; } if (connectionMethod === 'TNS') { - configDir = TNSpath; + configDir = resolveTnsConfigDir(TNSpath); + assertTnsConfigDir(configDir); + + const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); + tnsServicePort = resolvedTnsService?.port; + + assertTnsMtlsRequirements({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }); + + if (useMutualTls && !isMtlsPort(tnsServicePort)) { + logger({ + message: `mTLS is enabled but TNS service uses port ${tnsServicePort ?? 'unknown'} (not ${MTLS_PORT}). Connecting without wallet.`, + }); + } + + if (useMutualTls && isMtlsPort(tnsServicePort)) { + if (!hasWalletFiles(configDir)) { + throw new Error( + `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, + ); + } + + fixSqlNetOraWalletPath(path.join(configDir, 'sqlnet.ora'), configDir); + setPluginTnsAdmin(configDir); + } } if (clientType === 'InstantClient') { @@ -258,9 +497,11 @@ const connect = async ( } let connectString = ''; + const useTnsWallet = + connectionMethod === 'Wallet' || (connectionMethod === 'TNS' && useMutualTls && isMtlsPort(tnsServicePort)); if (['Wallet', 'TNS'].includes(connectionMethod)) { - connectString = getConnectionStringByTnsNames(configDir, serviceName, proxy, logger); + connectString = getConnectionStringByTnsNames(configDir, serviceName, proxy, logger, useTnsWallet); } else { connectString = getConnectionDescription( { @@ -309,20 +550,58 @@ const connect = async ( credentials.password = userPassword; } + const useWallet = + connectionMethod === 'Wallet' || + (connectionMethod === 'TNS' && + useMutualTls && + (isMtlsPort(tnsServicePort) || connectStringUsesMtlsPort(connectString))); + + if (connectionMethod === 'TNS' && useMutualTls && !useWallet) { + logger({ + message: + 'Skipping walletLocation, walletPassword, and configDir for thin connect (TNS service does not use mTLS port 1522).', + }); + } + + if (walletPassword && !useWallet) { + logger({ + message: + 'A wallet password is stored in the connection profile but is not sent to Oracle (mTLS disabled, non-mTLS port, or non-wallet connection method).', + }); + } + + const normalizedConnectString = normalizeConnectString(connectString); + const hostnameToResolve = connectionMethod === 'Basic' ? host : connectionInfo.host; + + if (!ssh && hostnameToResolve) { + await assertResolvableConnectHost(hostnameToResolve, logger); + } + + logger({ + message: 'Oracle connectString', + connectString: normalizedConnectString, + hostname: hostnameToResolve, + useWallet, + walletLocation: useWallet ? configDir : undefined, + configDir: useWallet ? configDir : undefined, + }); + return authByCredentials({ - connectString, + connectString: normalizedConnectString, username: userName, password: userPassword, queryRequestTimeout, authRole, - walletLocation: configDir, - walletPassword, + configDir: useWallet ? configDir : undefined, + walletLocation: useWallet ? configDir : undefined, + walletPassword: useWallet ? walletPassword : undefined, }); }; const disconnect = async sshService => { if (!connection) { - return Promise.reject(noConnectionError); + clearPluginTnsAdmin(); + return; } if (useSshTunnel) { @@ -333,6 +612,7 @@ const disconnect = async sshService => { return new Promise((resolve, reject) => { connection.close(err => { connection = null; + clearPluginTnsAdmin(); if (err) { return reject(err); } @@ -349,16 +629,21 @@ const authByCredentials = ({ authRole, walletPassword, walletLocation, + configDir, }) => { return new Promise((resolve, reject) => { - const connectionConfig = { - username, - password, - connectString, - privilege: authRole === 'default' ? undefined : oracleDB[authRole], - walletLocation, - walletPassword, - }; + const connectionConfig = _.omitBy( + { + username, + password, + connectString, + privilege: authRole === 'default' ? undefined : oracleDB[authRole], + walletLocation, + walletPassword, + configDir, + }, + _.isUndefined, + ); oracleDB.getConnection(connectionConfig, (err, conn) => { if (err) { connection = null; @@ -387,11 +672,7 @@ const getSchemaNames = async ({ includeSystemCollection, schemaName }, logger) = } else { query = `${selectStatement} WHERE ORACLE_MAINTAINED = 'N'${stmt ? ` AND ${stmt}` : ''}`; } - return await execute(query).catch(e => { - logger.info({ message: 'Cannot retrieve schema names' }); - logger.error(e); - return []; - }); + return execute(query); }; const pairToObj = pairs => { From 4c90dcb69af86fd74987f0592200a62f8c9a606e Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Fri, 22 May 2026 16:13:06 +0200 Subject: [PATCH 3/7] update lock file --- package-lock.json | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51f3ca23..02fcc340 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Oracle", - "version": "0.2.56", + "version": "0.2.60", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Oracle", - "version": "0.2.56", + "version": "0.2.60", "dependencies": { "adm-zip": "0.5.9", "async": "3.2.6", @@ -1043,9 +1043,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1271,7 +1271,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -1953,7 +1952,6 @@ "integrity": "sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "tsgolint": "bin/tsgolint.js" }, @@ -2089,7 +2087,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, From da3e73dc0ae10f2e4df1fc5e78797c11a46bc137 Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Fri, 22 May 2026 16:17:45 +0200 Subject: [PATCH 4/7] HCK-16220: reduce connect() cognitive complexity for Sonar. Extract TNS, wallet, SSH, and connect-string helpers so connect stays within Sonar limits. Co-authored-by: Cursor --- reverse_engineering/helpers/oracleHelper.js | 311 +++++++++++--------- 1 file changed, 174 insertions(+), 137 deletions(-) diff --git a/reverse_engineering/helpers/oracleHelper.js b/reverse_engineering/helpers/oracleHelper.js index a08e26c4..ddfba7a9 100644 --- a/reverse_engineering/helpers/oracleHelper.js +++ b/reverse_engineering/helpers/oracleHelper.js @@ -380,6 +380,135 @@ const getSshConnectionString = async (data, sshService, logger) => { ); }; +const trySyncTnsEndpointEarly = (connectionInfo, { connectionMethod, TNSpath, serviceName }, logger) => { + if (connectionMethod !== 'TNS' || !TNSpath) { + return; + } + + try { + const tnsConfigDir = resolveTnsConfigDir(TNSpath); + const tnsNamesOraFile = getTnsNamesOraFile(tnsConfigDir); + + if (tnsNamesOraFile && fs.existsSync(tnsNamesOraFile)) { + syncConnectionEndpointFromTns(connectionInfo, tnsConfigDir, serviceName, logger); + } + } catch (error) { + logger({ message: `Unable to sync host/port from tnsnames.ora: ${error.message}` }); + } +}; + +const assertBasicServiceName = (connectionMethod, serviceName) => { + if (connectionMethod === 'Basic' && !normalizeTnsAlias(serviceName)) { + throw new Error('Service name is required for Basic connection method.'); + } +}; + +const setupWalletConfigDir = async ({ walletFile, tempFolder, name }, connectionInfo, serviceName, logger) => { + const configDir = await extractWallet({ walletFile, tempFolder, name }); + setPluginTnsAdmin(configDir); + const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); + + return { configDir, tnsServicePort: resolvedTnsService?.port }; +}; + +const applyTnsMutualTlsWallet = (configDir, useMutualTls, tnsServicePort, logger) => { + if (useMutualTls && !isMtlsPort(tnsServicePort)) { + logger({ + message: `mTLS is enabled but TNS service uses port ${tnsServicePort ?? 'unknown'} (not ${MTLS_PORT}). Connecting without wallet.`, + }); + return; + } + + if (!useMutualTls || !isMtlsPort(tnsServicePort)) { + return; + } + + if (!hasWalletFiles(configDir)) { + throw new Error( + `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, + ); + } + + fixSqlNetOraWalletPath(path.join(configDir, 'sqlnet.ora'), configDir); + setPluginTnsAdmin(configDir); +}; + +const setupTnsConfigDir = (TNSpath, connectionInfo, { serviceName, useMutualTls, walletPassword }, logger) => { + const configDir = resolveTnsConfigDir(TNSpath); + assertTnsConfigDir(configDir); + + const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); + const tnsServicePort = resolvedTnsService?.port; + + assertTnsMtlsRequirements({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }); + applyTnsMutualTlsWallet(configDir, useMutualTls, tnsServicePort, logger); + + return { configDir, tnsServicePort }; +}; + +const resolveConnectionConfigDir = async ( + connectionMethod, + connectionInfo, + { walletFile, walletPassword, tempFolder, name, TNSpath, serviceName }, + useMutualTls, + logger, +) => { + if (connectionMethod === 'Wallet') { + return setupWalletConfigDir({ walletFile, tempFolder, name }, connectionInfo, serviceName, logger); + } + + if (connectionMethod === 'TNS') { + return setupTnsConfigDir(TNSpath, connectionInfo, { serviceName, useMutualTls, walletPassword }, logger); + } + + return { configDir: undefined, tnsServicePort: undefined }; +}; + +const buildSessionConnectString = ( + { connectionMethod, configDir, serviceName, proxy, useMutualTls, tnsServicePort, host, port, sid }, + logger, +) => { + const useTnsWallet = + connectionMethod === 'Wallet' || (connectionMethod === 'TNS' && useMutualTls && isMtlsPort(tnsServicePort)); + + if (['Wallet', 'TNS'].includes(connectionMethod)) { + return getConnectionStringByTnsNames(configDir, serviceName, proxy, logger, useTnsWallet); + } + + return getConnectionDescription({ host, port, sid, service: serviceName }, logger); +}; + +const applySshTunnelIfNeeded = async (ssh, connectString, tunnelParams, sshService, logger) => { + if (!ssh) { + return connectString; + } + + useSshTunnel = true; + return getSshConnectionString(tunnelParams, sshService, logger); +}; + +const shouldUseWalletForConnect = ({ connectionMethod, useMutualTls, tnsServicePort, connectString }) => + connectionMethod === 'Wallet' || + (connectionMethod === 'TNS' && + useMutualTls && + (isMtlsPort(tnsServicePort) || connectStringUsesMtlsPort(connectString))); + +const logWalletConnectNotes = ({ connectionMethod, useMutualTls, useWallet, walletPassword }, logger) => { + if (connectionMethod === 'TNS' && useMutualTls && !useWallet) { + logger({ + message: + 'Skipping walletLocation, walletPassword, and configDir for thin connect (TNS service does not use mTLS port 1522).', + }); + } + + if (walletPassword && !useWallet) { + logger({ + message: + 'A wallet password is stored in the connection profile but is not sent to Oracle (mTLS disabled, non-mTLS port, or non-wallet connection method).', + }); + } +}; + const connect = async (connectionInfo, sshService, logger) => { const { walletFile, @@ -412,18 +541,7 @@ const connect = async (connectionInfo, sshService, logger) => { mutualTLS, } = connectionInfo; - if (connectionMethod === 'TNS' && TNSpath) { - try { - const tnsConfigDir = resolveTnsConfigDir(TNSpath); - const tnsNamesOraFile = getTnsNamesOraFile(tnsConfigDir); - - if (tnsNamesOraFile && fs.existsSync(tnsNamesOraFile)) { - syncConnectionEndpointFromTns(connectionInfo, tnsConfigDir, serviceName, logger); - } - } catch (error) { - logger({ message: `Unable to sync host/port from tnsnames.ora: ${error.message}` }); - } - } + trySyncTnsEndpointEarly(connectionInfo, { connectionMethod, TNSpath, serviceName }, logger); if (connection) { logger({ message: 'Reusing existing Oracle connection' }); @@ -435,140 +553,59 @@ const connect = async (connectionInfo, sshService, logger) => { } const useMutualTls = isMutualTlsEnabled(mutualTLS); + assertBasicServiceName(connectionMethod, serviceName); - const MODES = { - thin: 'thin', - thick: 'thick', - }; - let configDir; - let libDir; - let credentials = {}; - let proxy = ''; - let tnsServicePort; - - if (connectionMethod === 'Basic' && !normalizeTnsAlias(serviceName)) { - throw new Error('Service name is required for Basic connection method.'); - } - - if (connectionMethod === 'Wallet') { - configDir = await extractWallet({ walletFile, tempFolder, name }); - setPluginTnsAdmin(configDir); - const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); - tnsServicePort = resolvedTnsService?.port; - } - - if (connectionMethod === 'TNS') { - configDir = resolveTnsConfigDir(TNSpath); - assertTnsConfigDir(configDir); - - const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); - tnsServicePort = resolvedTnsService?.port; - - assertTnsMtlsRequirements({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }); - - if (useMutualTls && !isMtlsPort(tnsServicePort)) { - logger({ - message: `mTLS is enabled but TNS service uses port ${tnsServicePort ?? 'unknown'} (not ${MTLS_PORT}). Connecting without wallet.`, - }); - } - - if (useMutualTls && isMtlsPort(tnsServicePort)) { - if (!hasWalletFiles(configDir)) { - throw new Error( - `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, - ); - } - - fixSqlNetOraWalletPath(path.join(configDir, 'sqlnet.ora'), configDir); - setPluginTnsAdmin(configDir); - } - } - - if (clientType === 'InstantClient') { - libDir = clientPath; - } + const { configDir, tnsServicePort } = await resolveConnectionConfigDir( + connectionMethod, + connectionInfo, + { walletFile, walletPassword, tempFolder, name, TNSpath, serviceName }, + useMutualTls, + logger, + ); - if (options?.proxy) { - proxy = parseProxyOptions(options?.proxy); - } + const libDir = clientType === 'InstantClient' ? clientPath : undefined; + const proxy = options?.proxy ? parseProxyOptions(options.proxy) : ''; - if (mode !== MODES.thin) { + if (mode !== 'thin') { oracleDB.initOracleClient({ libDir, configDir }); } - let connectString = ''; - const useTnsWallet = - connectionMethod === 'Wallet' || (connectionMethod === 'TNS' && useMutualTls && isMtlsPort(tnsServicePort)); - - if (['Wallet', 'TNS'].includes(connectionMethod)) { - connectString = getConnectionStringByTnsNames(configDir, serviceName, proxy, logger, useTnsWallet); - } else { - connectString = getConnectionDescription( - { - host, - port, - sid, - service: serviceName, - }, - logger, - ); - } + let connectString = buildSessionConnectString( + { connectionMethod, configDir, serviceName, proxy, useMutualTls, tnsServicePort, host, port, sid }, + logger, + ); - if (ssh) { - useSshTunnel = true; - connectString = await getSshConnectionString( - { - host, - port, - configDir, - serviceName, - sid, - connectionMethod, - sshConfig: { - ssh_user, - ssh_host, - ssh_port, - ssh_method, - ssh_key_file, - ssh_password, - ssh_key_passphrase, - }, + connectString = await applySshTunnelIfNeeded( + ssh, + connectString, + { + host, + port, + configDir, + serviceName, + sid, + connectionMethod, + sshConfig: { + ssh_user, + ssh_host, + ssh_port, + ssh_method, + ssh_key_file, + ssh_password, + ssh_key_passphrase, }, - sshService, - logger, - ); - } - - if (authMethod === 'OS') { - credentials.externalAuth = true; - } else if (authMethod === 'Kerberos') { - credentials.username = userName; - credentials.password = userPassword; - credentials.externalAuth = true; - } else { - credentials.username = userName; - credentials.password = userPassword; - } - - const useWallet = - connectionMethod === 'Wallet' || - (connectionMethod === 'TNS' && - useMutualTls && - (isMtlsPort(tnsServicePort) || connectStringUsesMtlsPort(connectString))); - - if (connectionMethod === 'TNS' && useMutualTls && !useWallet) { - logger({ - message: - 'Skipping walletLocation, walletPassword, and configDir for thin connect (TNS service does not use mTLS port 1522).', - }); - } + }, + sshService, + logger, + ); - if (walletPassword && !useWallet) { - logger({ - message: - 'A wallet password is stored in the connection profile but is not sent to Oracle (mTLS disabled, non-mTLS port, or non-wallet connection method).', - }); - } + const useWallet = shouldUseWalletForConnect({ + connectionMethod, + useMutualTls, + tnsServicePort, + connectString, + }); + logWalletConnectNotes({ connectionMethod, useMutualTls, useWallet, walletPassword }, logger); const normalizedConnectString = normalizeConnectString(connectString); const hostnameToResolve = connectionMethod === 'Basic' ? host : connectionInfo.host; From fd78e1b19ad7a3bf65c7b4854afae67558e6bf01 Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Tue, 26 May 2026 15:22:24 +0200 Subject: [PATCH 5/7] HCK-16220: clarify connection modal copy for mTLS and TNS. Co-authored-by: Cursor --- .../connectionSettingsModalConfig.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json index 845398dc..b2763125 100644 --- a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json +++ b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json @@ -101,7 +101,7 @@ { "inputLabel": "Wallet archive", "inputKeyword": "walletFile", - "description": "Specify the full path location and name of the wallet zip file", + "description": "Specify the full path location and name of the wallet zip file.", "inputType": "file", "extensions": ["zip"], "dependency": { @@ -124,7 +124,7 @@ { "inputLabel": "Mutual TLS (mTLS)", "inputKeyword": "mutualTLS", - "description": "Enable only when the database requires a client wallet (mutual TLS). Leave unchecked for legacy TNS connections that use a tnsnames.ora directory with username/password only.", + "description": "Enable only when the database requires a client certificate (cloud wallet, mutual TLS).", "inputType": "checkbox", "defaultValue": false, "dependency": { @@ -149,7 +149,7 @@ { "inputLabel": "Service Name", "inputKeyword": "serviceName", - "description": "Specify the service name of the Oracle Instance", + "description": "Specify the service name of the Oracle Instance.", "inputType": "text", "regex": "([^\\s])", "dependency": { @@ -169,7 +169,6 @@ { "inputLabel": "Wallet Password", "inputKeyword": "walletPassword", - "description": "OCI wallet password (not the database user password). Required for Cloud Wallet and TNS with mTLS enabled.", "inputType": "password", "isHiddenKey": true, "dependency": { @@ -211,7 +210,7 @@ { "inputLabel": "TNS alias", "inputKeyword": "serviceName", - "description": "Optional. Leave empty to use the first entry in tnsnames.ora (e.g. _high).", + "description": "Leave empty to use the first entry in tnsnames.ora (e.g. _high).", "inputType": "text", "inputPlaceholder": "Optional TNS alias", "dependency": { @@ -222,7 +221,7 @@ { "inputLabel": "SID", "inputKeyword": "sid", - "description": "Optionally specify the SID of the Oracle Instance", + "description": "Optionally specify the SID of the Oracle Instance.", "inputType": "text", "dependency": { "type": "and", From c31dd352600434df6e9f8957bb1201815301cfe2 Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Tue, 26 May 2026 15:45:49 +0200 Subject: [PATCH 6/7] HCK-16220: extract TNS and mTLS helpers from oracleHelper. Move tnsnames resolution, mTLS checks, and connect-string setup into focused modules without behavior changes. Co-authored-by: Cursor --- .../helpers/connectStringDescription.js | 32 ++ reverse_engineering/helpers/oracleHelper.js | 394 +----------------- reverse_engineering/helpers/tns/tnsAdmin.js | 19 + reverse_engineering/helpers/tns/tnsConfig.js | 75 ++++ .../helpers/tns/tnsConnectString.js | 122 ++++++ .../helpers/tns/tnsConnectionSetup.js | 143 +++++++ reverse_engineering/helpers/tns/tnsMtls.js | 60 +++ 7 files changed, 462 insertions(+), 383 deletions(-) create mode 100644 reverse_engineering/helpers/connectStringDescription.js create mode 100644 reverse_engineering/helpers/tns/tnsAdmin.js create mode 100644 reverse_engineering/helpers/tns/tnsConfig.js create mode 100644 reverse_engineering/helpers/tns/tnsConnectString.js create mode 100644 reverse_engineering/helpers/tns/tnsConnectionSetup.js create mode 100644 reverse_engineering/helpers/tns/tnsMtls.js diff --git a/reverse_engineering/helpers/connectStringDescription.js b/reverse_engineering/helpers/connectStringDescription.js new file mode 100644 index 00000000..8745fd6e --- /dev/null +++ b/reverse_engineering/helpers/connectStringDescription.js @@ -0,0 +1,32 @@ +const combine = (val, str) => (val ? str : ''); + +const normalizeConnectString = connectString => + typeof connectString === 'string' ? connectString.replace(/\s+/g, '') : connectString; + +const getConnectionDescription = ( + { protocol, host, port, sid, service, httpsProxy, httpsProxyPort, retryCount, retryDelay, sslServerDnMatch }, + logger, +) => { + const connectionString = normalizeConnectString(`(DESCRIPTION= + ${combine(retryCount, `(RETRY_COUNT=${retryCount})`)} + ${combine(retryDelay, `(RETRY_DELAY=${retryDelay})`)} + (ADDRESS= + (PROTOCOL=${protocol || 'tcp'}) + (HOST=${host}) + (PORT=${port})) + ${combine(httpsProxy, `(HTTPS_PROXY=${httpsProxy})`)} + ${combine(httpsProxyPort, `(HTTPS_PROXY_PORT=${httpsProxyPort})`)} + (CONNECT_DATA= + ${combine(sid, `(SID=${sid})`)} + ${combine(service, `(SERVICE_NAME=${service})`)} + ) + ${combine(sslServerDnMatch, `(SECURITY=(SSL_SERVER_DN_MATCH=${sslServerDnMatch}))`)} + )`); + logger({ message: 'connectString', connectString: connectionString }); + return connectionString; +}; + +module.exports = { + normalizeConnectString, + getConnectionDescription, +}; diff --git a/reverse_engineering/helpers/oracleHelper.js b/reverse_engineering/helpers/oracleHelper.js index ddfba7a9..b4da2676 100644 --- a/reverse_engineering/helpers/oracleHelper.js +++ b/reverse_engineering/helpers/oracleHelper.js @@ -1,34 +1,26 @@ const _ = require('lodash'); const dns = require('dns'); const dnsPromises = dns.promises; -const fs = require('fs'); const net = require('net'); -const path = require('path'); const oracleDB = require('oracledb'); -const extractWallet = require('./extractWallet'); -const fixSqlNetOraWalletPath = require('./extractWallet').fixSqlNetOraWalletPath; -const parseTns = require('./parseTns'); const { getSchemaSequences } = require('./getSchemaSequences'); const { getSchemaSynonyms } = require('./getSchemaSynonyms'); +const { normalizeConnectString, getConnectionDescription } = require('./connectStringDescription'); +const { clearPluginTnsAdmin } = require('./tns/tnsAdmin'); +const { normalizeTnsAlias, getResolvedTnsService } = require('./tns/tnsConnectString'); +const { + trySyncTnsEndpointEarly, + resolveConnectionConfigDir, + buildSessionConnectString, + shouldUseWalletForConnect, + logWalletConnectNotes, + isMutualTlsEnabled, +} = require('./tns/tnsConnectionSetup'); const noConnectionError = { message: 'Connection error' }; let connection; let useSshTunnel; -let pluginTnsAdmin; - -const setPluginTnsAdmin = configDir => { - pluginTnsAdmin = configDir; - process.env.TNS_ADMIN = configDir; -}; - -const clearPluginTnsAdmin = () => { - if (pluginTnsAdmin && process.env.TNS_ADMIN === pluginTnsAdmin) { - delete process.env.TNS_ADMIN; - } - - pluginTnsAdmin = null; -}; const parseProxyOptions = (proxyString = '') => { const result = proxyString.match(/http:\/\/(?:.*?:.*?@)?(.*?):(\d+)/i); @@ -43,233 +35,6 @@ const parseProxyOptions = (proxyString = '') => { }; }; -const TNS_NAMES_FILE = 'tnsnames.ora'; - -const resolveTnsConfigDir = tnsPath => { - if (!tnsPath) { - return tnsPath; - } - - const normalizedPath = path.normalize(String(tnsPath).trim()); - - if (!fs.existsSync(normalizedPath)) { - return normalizedPath; - } - - if (fs.statSync(normalizedPath).isDirectory()) { - return normalizedPath; - } - - if (path.basename(normalizedPath).toLowerCase() === TNS_NAMES_FILE) { - return path.dirname(normalizedPath); - } - - throw new Error(`Invalid TNS path "${normalizedPath}". Select the wallet directory or the ${TNS_NAMES_FILE} file.`); -}; - -const assertTnsConfigDir = configDir => { - const tnsNamesOraFile = getTnsNamesOraFile(configDir); - - if (!tnsNamesOraFile || !fs.existsSync(tnsNamesOraFile)) { - throw new Error( - `Cannot find ${TNS_NAMES_FILE} in "${configDir}". Select the wallet directory or the ${TNS_NAMES_FILE} file.`, - ); - } -}; - -const getTnsNamesOraFile = configDir => { - const resolvedConfigDir = resolveTnsConfigDir(configDir); - const tnsNamesOraFile = [ - resolvedConfigDir, - process.env.TNS_ADMIN, - path.join(process.env.ORACLE_HOME || '', 'network', 'admin'), - path.join(process.env.LD_LIBRARY_PATH || '', 'network', 'admin'), - ].reduce((filePath, configFolder) => { - if (filePath) { - return filePath; - } - - let file = path.join(configFolder, 'tnsnames.ora'); - - if (fs.existsSync(file)) { - return file; - } else { - return filePath; - } - }, ''); - - return tnsNamesOraFile; -}; - -const parseTnsNamesOra = filePath => { - const content = fs.readFileSync(filePath).toString(); - const result = parseTns(content); - return result; -}; - -const WALLET_FILES = ['ewallet.pem', 'cwallet.sso', 'ewallet.p12']; -const MTLS_PORT = '1522'; - -const hasWalletFiles = configDir => - configDir && fs.existsSync(configDir) && WALLET_FILES.some(file => fs.existsSync(path.join(configDir, file))); - -const hasAutoLoginWallet = configDir => configDir && fs.existsSync(path.join(configDir, 'cwallet.sso')); - -const isMutualTlsEnabled = mutualTLS => mutualTLS === true || mutualTLS === 'true'; - -const assertTnsMtlsRequirements = ({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }) => { - if (!useMutualTls) { - if (isMtlsPort(tnsServicePort)) { - logger({ - message: `TNS service uses port ${MTLS_PORT} without mutual TLS enabled. Connecting in legacy TNS mode (server TLS only, no wallet). Enable "Mutual TLS (mTLS)" if the database requires a client wallet.`, - }); - } - - return; - } - - if (!isMtlsPort(tnsServicePort)) { - return; - } - - if (!hasWalletFiles(configDir)) { - throw new Error( - `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, - ); - } - - if (!walletPassword && !hasAutoLoginWallet(configDir)) { - throw new Error( - `Mutual TLS requires a wallet password (OCI wallet zip password), unless the directory contains an auto-login wallet (cwallet.sso).`, - ); - } - - if (!walletPassword && hasAutoLoginWallet(configDir)) { - logger({ - message: 'Using auto-login wallet (cwallet.sso); wallet password not required.', - }); - } -}; - -const isMtlsPort = port => String(port) === MTLS_PORT; - -const connectStringUsesMtlsPort = connectString => /\(PORT\s*=\s*1522\)/i.test(connectString); - -const normalizeTnsAlias = serviceName => (serviceName == null ? '' : String(serviceName).trim()); - -const getResolvedTnsService = (configDir, serviceName, logger) => { - const tnsAlias = normalizeTnsAlias(serviceName); - const filePath = getTnsNamesOraFile(configDir); - - if (!fs.existsSync(filePath)) { - return null; - } - - logger({ message: 'Found tnsnames.ora file: ' + filePath }); - - const tnsData = parseTnsNamesOra(filePath); - - logger({ message: 'tnsnames.ora successfully parsed' }); - const tnsServicesNames = Object.keys(tnsData); - - if (tnsServicesNames.length === 0) { - logger({ message: 'No TNS services found in tnsnames.ora' }); - return null; - } - - const [firstTnsServiceName] = tnsServicesNames; - const tnsService = (tnsAlias && tnsData[tnsAlias]) || tnsData[firstTnsServiceName]; - - if (!tnsAlias) { - logger({ - message: `No TNS alias provided. Using first TNS service ${firstTnsServiceName} from ${path.join(configDir, 'tnsnames.ora')}.`, - }); - } else if (!tnsData[tnsAlias]) { - logger({ - message: `TNS alias '${tnsAlias}' not found. Using first TNS service ${firstTnsServiceName} from ${path.join(configDir, 'tnsnames.ora')}.`, - }); - } else { - logger({ - message: `Connect using TNS service ${tnsAlias} from ${path.join(configDir, 'tnsnames.ora')}.`, - }); - } - - const description = tnsService?.data?.description; - const address = description?.address; - const resolvedAlias = tnsAlias && tnsData[tnsAlias] ? tnsAlias : firstTnsServiceName; - - return { - description, - address, - service: description?.connect_data?.service_name, - sid: description?.connect_data?.sid, - port: address?.port, - resolvedAlias, - }; -}; - -const syncConnectionEndpointFromTns = (connectionInfo, configDir, serviceName, logger) => { - const resolved = getResolvedTnsService(configDir, serviceName, logger); - - if (!resolved?.address?.host) { - return resolved; - } - - connectionInfo.host = resolved.address.host; - connectionInfo.port = resolved.address.port; - - logger({ - message: 'Synced connection host/port from tnsnames.ora for connections list', - host: connectionInfo.host, - port: connectionInfo.port, - tnsAlias: resolved.resolvedAlias, - }); - - return resolved; -}; - -const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger, useWallet = false) => { - const resolved = getResolvedTnsService(configDir, serviceName, logger); - - if (!resolved) { - return serviceName; - } - - const { description, address, service, sid, port, resolvedAlias } = resolved; - - logger({ message: 'tnsnames.ora', address, service, port }); - - if (useWallet) { - logger({ - message: 'Using TNS alias with mTLS wallet', - connectString: resolvedAlias, - }); - return resolvedAlias; - } - - return getConnectionDescription( - _.omitBy( - { - ...address, - ...proxy, - protocol: address?.protocol || 'tcps', - service: service || serviceName, - sid, - retryCount: description?.retry_count, - retryDelay: description?.retry_delay, - sslServerDnMatch: description?.security?.ssl_server_dn_match, - }, - _.isUndefined, - ), - logger, - ); -}; - -const combine = (val, str) => (val ? str : ''); - -const normalizeConnectString = connectString => - typeof connectString === 'string' ? connectString.replace(/\s+/g, '') : connectString; - const UNUSABLE_RESOLVED_HOSTS = new Set(['255.255.255.255', '0.0.0.0']); const assertResolvableConnectHost = async (hostname, logger) => { @@ -302,29 +67,6 @@ const assertResolvableConnectHost = async (hostname, logger) => { } }; -const getConnectionDescription = ( - { protocol, host, port, sid, service, httpsProxy, httpsProxyPort, retryCount, retryDelay, sslServerDnMatch }, - logger, -) => { - const connectionString = normalizeConnectString(`(DESCRIPTION= - ${combine(retryCount, `(RETRY_COUNT=${retryCount})`)} - ${combine(retryDelay, `(RETRY_DELAY=${retryDelay})`)} - (ADDRESS= - (PROTOCOL=${protocol || 'tcp'}) - (HOST=${host}) - (PORT=${port})) - ${combine(httpsProxy, `(HTTPS_PROXY=${httpsProxy})`)} - ${combine(httpsProxyPort, `(HTTPS_PROXY_PORT=${httpsProxyPort})`)} - (CONNECT_DATA= - ${combine(sid, `(SID=${sid})`)} - ${combine(service, `(SERVICE_NAME=${service})`)} - ) - ${combine(sslServerDnMatch, `(SECURITY=(SSL_SERVER_DN_MATCH=${sslServerDnMatch}))`)} - )`); - logger({ message: 'connectString', connectString: connectionString }); - return connectionString; -}; - const getSshConnectionString = async (data, sshService, logger) => { let connectionData = { protocol: '', @@ -380,104 +122,12 @@ const getSshConnectionString = async (data, sshService, logger) => { ); }; -const trySyncTnsEndpointEarly = (connectionInfo, { connectionMethod, TNSpath, serviceName }, logger) => { - if (connectionMethod !== 'TNS' || !TNSpath) { - return; - } - - try { - const tnsConfigDir = resolveTnsConfigDir(TNSpath); - const tnsNamesOraFile = getTnsNamesOraFile(tnsConfigDir); - - if (tnsNamesOraFile && fs.existsSync(tnsNamesOraFile)) { - syncConnectionEndpointFromTns(connectionInfo, tnsConfigDir, serviceName, logger); - } - } catch (error) { - logger({ message: `Unable to sync host/port from tnsnames.ora: ${error.message}` }); - } -}; - const assertBasicServiceName = (connectionMethod, serviceName) => { if (connectionMethod === 'Basic' && !normalizeTnsAlias(serviceName)) { throw new Error('Service name is required for Basic connection method.'); } }; -const setupWalletConfigDir = async ({ walletFile, tempFolder, name }, connectionInfo, serviceName, logger) => { - const configDir = await extractWallet({ walletFile, tempFolder, name }); - setPluginTnsAdmin(configDir); - const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); - - return { configDir, tnsServicePort: resolvedTnsService?.port }; -}; - -const applyTnsMutualTlsWallet = (configDir, useMutualTls, tnsServicePort, logger) => { - if (useMutualTls && !isMtlsPort(tnsServicePort)) { - logger({ - message: `mTLS is enabled but TNS service uses port ${tnsServicePort ?? 'unknown'} (not ${MTLS_PORT}). Connecting without wallet.`, - }); - return; - } - - if (!useMutualTls || !isMtlsPort(tnsServicePort)) { - return; - } - - if (!hasWalletFiles(configDir)) { - throw new Error( - `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, - ); - } - - fixSqlNetOraWalletPath(path.join(configDir, 'sqlnet.ora'), configDir); - setPluginTnsAdmin(configDir); -}; - -const setupTnsConfigDir = (TNSpath, connectionInfo, { serviceName, useMutualTls, walletPassword }, logger) => { - const configDir = resolveTnsConfigDir(TNSpath); - assertTnsConfigDir(configDir); - - const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); - const tnsServicePort = resolvedTnsService?.port; - - assertTnsMtlsRequirements({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }); - applyTnsMutualTlsWallet(configDir, useMutualTls, tnsServicePort, logger); - - return { configDir, tnsServicePort }; -}; - -const resolveConnectionConfigDir = async ( - connectionMethod, - connectionInfo, - { walletFile, walletPassword, tempFolder, name, TNSpath, serviceName }, - useMutualTls, - logger, -) => { - if (connectionMethod === 'Wallet') { - return setupWalletConfigDir({ walletFile, tempFolder, name }, connectionInfo, serviceName, logger); - } - - if (connectionMethod === 'TNS') { - return setupTnsConfigDir(TNSpath, connectionInfo, { serviceName, useMutualTls, walletPassword }, logger); - } - - return { configDir: undefined, tnsServicePort: undefined }; -}; - -const buildSessionConnectString = ( - { connectionMethod, configDir, serviceName, proxy, useMutualTls, tnsServicePort, host, port, sid }, - logger, -) => { - const useTnsWallet = - connectionMethod === 'Wallet' || (connectionMethod === 'TNS' && useMutualTls && isMtlsPort(tnsServicePort)); - - if (['Wallet', 'TNS'].includes(connectionMethod)) { - return getConnectionStringByTnsNames(configDir, serviceName, proxy, logger, useTnsWallet); - } - - return getConnectionDescription({ host, port, sid, service: serviceName }, logger); -}; - const applySshTunnelIfNeeded = async (ssh, connectString, tunnelParams, sshService, logger) => { if (!ssh) { return connectString; @@ -487,28 +137,6 @@ const applySshTunnelIfNeeded = async (ssh, connectString, tunnelParams, sshServi return getSshConnectionString(tunnelParams, sshService, logger); }; -const shouldUseWalletForConnect = ({ connectionMethod, useMutualTls, tnsServicePort, connectString }) => - connectionMethod === 'Wallet' || - (connectionMethod === 'TNS' && - useMutualTls && - (isMtlsPort(tnsServicePort) || connectStringUsesMtlsPort(connectString))); - -const logWalletConnectNotes = ({ connectionMethod, useMutualTls, useWallet, walletPassword }, logger) => { - if (connectionMethod === 'TNS' && useMutualTls && !useWallet) { - logger({ - message: - 'Skipping walletLocation, walletPassword, and configDir for thin connect (TNS service does not use mTLS port 1522).', - }); - } - - if (walletPassword && !useWallet) { - logger({ - message: - 'A wallet password is stored in the connection profile but is not sent to Oracle (mTLS disabled, non-mTLS port, or non-wallet connection method).', - }); - } -}; - const connect = async (connectionInfo, sshService, logger) => { const { walletFile, diff --git a/reverse_engineering/helpers/tns/tnsAdmin.js b/reverse_engineering/helpers/tns/tnsAdmin.js new file mode 100644 index 00000000..8fbf5556 --- /dev/null +++ b/reverse_engineering/helpers/tns/tnsAdmin.js @@ -0,0 +1,19 @@ +let pluginTnsAdmin; + +const setPluginTnsAdmin = configDir => { + pluginTnsAdmin = configDir; + process.env.TNS_ADMIN = configDir; +}; + +const clearPluginTnsAdmin = () => { + if (pluginTnsAdmin && process.env.TNS_ADMIN === pluginTnsAdmin) { + delete process.env.TNS_ADMIN; + } + + pluginTnsAdmin = null; +}; + +module.exports = { + setPluginTnsAdmin, + clearPluginTnsAdmin, +}; diff --git a/reverse_engineering/helpers/tns/tnsConfig.js b/reverse_engineering/helpers/tns/tnsConfig.js new file mode 100644 index 00000000..c48e41c8 --- /dev/null +++ b/reverse_engineering/helpers/tns/tnsConfig.js @@ -0,0 +1,75 @@ +const fs = require('fs'); +const path = require('path'); +const parseTns = require('../parseTns'); + +const TNS_NAMES_FILE = 'tnsnames.ora'; + +const resolveTnsConfigDir = tnsPath => { + if (!tnsPath) { + return tnsPath; + } + + const normalizedPath = path.normalize(String(tnsPath).trim()); + + if (!fs.existsSync(normalizedPath)) { + return normalizedPath; + } + + if (fs.statSync(normalizedPath).isDirectory()) { + return normalizedPath; + } + + if (path.basename(normalizedPath).toLowerCase() === TNS_NAMES_FILE) { + return path.dirname(normalizedPath); + } + + throw new Error(`Invalid TNS path "${normalizedPath}". Select the wallet directory or the ${TNS_NAMES_FILE} file.`); +}; + +const getTnsNamesOraFile = configDir => { + const resolvedConfigDir = resolveTnsConfigDir(configDir); + const tnsNamesOraFile = [ + resolvedConfigDir, + process.env.TNS_ADMIN, + path.join(process.env.ORACLE_HOME || '', 'network', 'admin'), + path.join(process.env.LD_LIBRARY_PATH || '', 'network', 'admin'), + ].reduce((filePath, configFolder) => { + if (filePath) { + return filePath; + } + + let file = path.join(configFolder, 'tnsnames.ora'); + + if (fs.existsSync(file)) { + return file; + } else { + return filePath; + } + }, ''); + + return tnsNamesOraFile; +}; + +const assertTnsConfigDir = configDir => { + const tnsNamesOraFile = getTnsNamesOraFile(configDir); + + if (!tnsNamesOraFile || !fs.existsSync(tnsNamesOraFile)) { + throw new Error( + `Cannot find ${TNS_NAMES_FILE} in "${configDir}". Select the wallet directory or the ${TNS_NAMES_FILE} file.`, + ); + } +}; + +const parseTnsNamesOra = filePath => { + const content = fs.readFileSync(filePath).toString(); + const result = parseTns(content); + return result; +}; + +module.exports = { + TNS_NAMES_FILE, + resolveTnsConfigDir, + assertTnsConfigDir, + getTnsNamesOraFile, + parseTnsNamesOra, +}; diff --git a/reverse_engineering/helpers/tns/tnsConnectString.js b/reverse_engineering/helpers/tns/tnsConnectString.js new file mode 100644 index 00000000..ab904952 --- /dev/null +++ b/reverse_engineering/helpers/tns/tnsConnectString.js @@ -0,0 +1,122 @@ +const fs = require('fs'); +const path = require('path'); +const _ = require('lodash'); +const { getConnectionDescription } = require('../connectStringDescription'); +const { getTnsNamesOraFile, parseTnsNamesOra } = require('./tnsConfig'); + +const normalizeTnsAlias = serviceName => (serviceName == null ? '' : String(serviceName).trim()); + +const getResolvedTnsService = (configDir, serviceName, logger) => { + const tnsAlias = normalizeTnsAlias(serviceName); + const filePath = getTnsNamesOraFile(configDir); + + if (!fs.existsSync(filePath)) { + return null; + } + + logger({ message: 'Found tnsnames.ora file: ' + filePath }); + + const tnsData = parseTnsNamesOra(filePath); + + logger({ message: 'tnsnames.ora successfully parsed' }); + const tnsServicesNames = Object.keys(tnsData); + + if (tnsServicesNames.length === 0) { + logger({ message: 'No TNS services found in tnsnames.ora' }); + return null; + } + + const [firstTnsServiceName] = tnsServicesNames; + const tnsService = (tnsAlias && tnsData[tnsAlias]) || tnsData[firstTnsServiceName]; + + if (!tnsAlias) { + logger({ + message: `No TNS alias provided. Using first TNS service ${firstTnsServiceName} from ${path.join(configDir, 'tnsnames.ora')}.`, + }); + } else if (!tnsData[tnsAlias]) { + logger({ + message: `TNS alias '${tnsAlias}' not found. Using first TNS service ${firstTnsServiceName} from ${path.join(configDir, 'tnsnames.ora')}.`, + }); + } else { + logger({ + message: `Connect using TNS service ${tnsAlias} from ${path.join(configDir, 'tnsnames.ora')}.`, + }); + } + + const description = tnsService?.data?.description; + const address = description?.address; + const resolvedAlias = tnsAlias && tnsData[tnsAlias] ? tnsAlias : firstTnsServiceName; + + return { + description, + address, + service: description?.connect_data?.service_name, + sid: description?.connect_data?.sid, + port: address?.port, + resolvedAlias, + }; +}; + +const syncConnectionEndpointFromTns = (connectionInfo, configDir, serviceName, logger) => { + const resolved = getResolvedTnsService(configDir, serviceName, logger); + + if (!resolved?.address?.host) { + return resolved; + } + + connectionInfo.host = resolved.address.host; + connectionInfo.port = resolved.address.port; + + logger({ + message: 'Synced connection host/port from tnsnames.ora for connections list', + host: connectionInfo.host, + port: connectionInfo.port, + tnsAlias: resolved.resolvedAlias, + }); + + return resolved; +}; + +const getConnectionStringByTnsNames = (configDir, serviceName, proxy, logger, useWallet = false) => { + const resolved = getResolvedTnsService(configDir, serviceName, logger); + + if (!resolved) { + return serviceName; + } + + const { description, address, service, sid, port, resolvedAlias } = resolved; + + logger({ message: 'tnsnames.ora', address, service, port }); + + if (useWallet) { + logger({ + message: 'Using TNS alias with mTLS wallet', + connectString: resolvedAlias, + }); + return resolvedAlias; + } + + return getConnectionDescription( + _.omitBy( + { + ...address, + ...proxy, + protocol: address?.protocol || 'tcps', + service: service || serviceName, + sid, + retryCount: description?.retry_count, + retryDelay: description?.retry_delay, + sslServerDnMatch: description?.security?.ssl_server_dn_match, + }, + _.isUndefined, + ), + logger, + ); +}; + +module.exports = { + normalizeTnsAlias, + getResolvedTnsService, + syncConnectionEndpointFromTns, + getConnectionStringByTnsNames, +}; diff --git a/reverse_engineering/helpers/tns/tnsConnectionSetup.js b/reverse_engineering/helpers/tns/tnsConnectionSetup.js new file mode 100644 index 00000000..04f672b3 --- /dev/null +++ b/reverse_engineering/helpers/tns/tnsConnectionSetup.js @@ -0,0 +1,143 @@ +const fs = require('fs'); +const path = require('path'); +const extractWallet = require('../extractWallet'); +const { fixSqlNetOraWalletPath } = require('../extractWallet'); +const { setPluginTnsAdmin } = require('./tnsAdmin'); +const { resolveTnsConfigDir, assertTnsConfigDir, getTnsNamesOraFile } = require('./tnsConfig'); +const { + WALLET_FILES, + MTLS_PORT, + hasWalletFiles, + isMutualTlsEnabled, + isMtlsPort, + connectStringUsesMtlsPort, + assertTnsMtlsRequirements, +} = require('./tnsMtls'); +const { syncConnectionEndpointFromTns, getConnectionStringByTnsNames } = require('./tnsConnectString'); +const { getConnectionDescription } = require('../connectStringDescription'); + +const trySyncTnsEndpointEarly = (connectionInfo, { connectionMethod, TNSpath, serviceName }, logger) => { + if (connectionMethod !== 'TNS' || !TNSpath) { + return; + } + + try { + const tnsConfigDir = resolveTnsConfigDir(TNSpath); + const tnsNamesOraFile = getTnsNamesOraFile(tnsConfigDir); + + if (tnsNamesOraFile && fs.existsSync(tnsNamesOraFile)) { + syncConnectionEndpointFromTns(connectionInfo, tnsConfigDir, serviceName, logger); + } + } catch (error) { + logger({ message: `Unable to sync host/port from tnsnames.ora: ${error.message}` }); + } +}; + +const setupWalletConfigDir = async ({ walletFile, tempFolder, name }, connectionInfo, serviceName, logger) => { + const configDir = await extractWallet({ walletFile, tempFolder, name }); + setPluginTnsAdmin(configDir); + const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); + + return { configDir, tnsServicePort: resolvedTnsService?.port }; +}; + +const applyTnsMutualTlsWallet = (configDir, useMutualTls, tnsServicePort, logger) => { + if (useMutualTls && !isMtlsPort(tnsServicePort)) { + logger({ + message: `mTLS is enabled but TNS service uses port ${tnsServicePort ?? 'unknown'} (not ${MTLS_PORT}). Connecting without wallet.`, + }); + return; + } + + if (!useMutualTls || !isMtlsPort(tnsServicePort)) { + return; + } + + if (!hasWalletFiles(configDir)) { + throw new Error( + `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, + ); + } + + fixSqlNetOraWalletPath(path.join(configDir, 'sqlnet.ora'), configDir); + setPluginTnsAdmin(configDir); +}; + +const setupTnsConfigDir = (TNSpath, connectionInfo, { serviceName, useMutualTls, walletPassword }, logger) => { + const configDir = resolveTnsConfigDir(TNSpath); + assertTnsConfigDir(configDir); + + const resolvedTnsService = syncConnectionEndpointFromTns(connectionInfo, configDir, serviceName, logger); + const tnsServicePort = resolvedTnsService?.port; + + assertTnsMtlsRequirements({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }); + applyTnsMutualTlsWallet(configDir, useMutualTls, tnsServicePort, logger); + + return { configDir, tnsServicePort }; +}; + +const resolveConnectionConfigDir = async ( + connectionMethod, + connectionInfo, + { walletFile, walletPassword, tempFolder, name, TNSpath, serviceName }, + useMutualTls, + logger, +) => { + if (connectionMethod === 'Wallet') { + return setupWalletConfigDir({ walletFile, tempFolder, name }, connectionInfo, serviceName, logger); + } + + if (connectionMethod === 'TNS') { + return setupTnsConfigDir(TNSpath, connectionInfo, { serviceName, useMutualTls, walletPassword }, logger); + } + + return { configDir: undefined, tnsServicePort: undefined }; +}; + +const buildSessionConnectString = ( + { connectionMethod, configDir, serviceName, proxy, useMutualTls, tnsServicePort, host, port, sid }, + logger, +) => { + const useTnsWallet = + connectionMethod === 'Wallet' || (connectionMethod === 'TNS' && useMutualTls && isMtlsPort(tnsServicePort)); + + if (['Wallet', 'TNS'].includes(connectionMethod)) { + return getConnectionStringByTnsNames(configDir, serviceName, proxy, logger, useTnsWallet); + } + + return getConnectionDescription({ host, port, sid, service: serviceName }, logger); +}; + +const shouldUseWalletForConnect = ({ connectionMethod, useMutualTls, tnsServicePort, connectString }) => + connectionMethod === 'Wallet' || + (connectionMethod === 'TNS' && + useMutualTls && + (isMtlsPort(tnsServicePort) || connectStringUsesMtlsPort(connectString))); + +const logWalletConnectNotes = ({ connectionMethod, useMutualTls, useWallet, walletPassword }, logger) => { + if (connectionMethod === 'TNS' && useMutualTls && !useWallet) { + logger({ + message: + 'Skipping walletLocation, walletPassword, and configDir for thin connect (TNS service does not use mTLS port 1522).', + }); + } + + if (walletPassword && !useWallet) { + logger({ + message: + 'A wallet password is stored in the connection profile but is not sent to Oracle (mTLS disabled, non-mTLS port, or non-wallet connection method).', + }); + } +}; + +module.exports = { + trySyncTnsEndpointEarly, + setupWalletConfigDir, + applyTnsMutualTlsWallet, + setupTnsConfigDir, + resolveConnectionConfigDir, + buildSessionConnectString, + shouldUseWalletForConnect, + logWalletConnectNotes, + isMutualTlsEnabled, +}; diff --git a/reverse_engineering/helpers/tns/tnsMtls.js b/reverse_engineering/helpers/tns/tnsMtls.js new file mode 100644 index 00000000..2337a4a3 --- /dev/null +++ b/reverse_engineering/helpers/tns/tnsMtls.js @@ -0,0 +1,60 @@ +const fs = require('fs'); +const path = require('path'); + +const WALLET_FILES = ['ewallet.pem', 'cwallet.sso', 'ewallet.p12']; +const MTLS_PORT = '1522'; + +const hasWalletFiles = configDir => + configDir && fs.existsSync(configDir) && WALLET_FILES.some(file => fs.existsSync(path.join(configDir, file))); + +const hasAutoLoginWallet = configDir => configDir && fs.existsSync(path.join(configDir, 'cwallet.sso')); + +const isMutualTlsEnabled = mutualTLS => mutualTLS === true || mutualTLS === 'true'; + +const isMtlsPort = port => String(port) === MTLS_PORT; + +const connectStringUsesMtlsPort = connectString => /\(PORT\s*=\s*1522\)/i.test(connectString); + +const assertTnsMtlsRequirements = ({ configDir, tnsServicePort, useMutualTls, walletPassword, logger }) => { + if (!useMutualTls) { + if (isMtlsPort(tnsServicePort)) { + logger({ + message: `TNS service uses port ${MTLS_PORT} without mutual TLS enabled. Connecting in legacy TNS mode (server TLS only, no wallet). Enable "Mutual TLS (mTLS)" if the database requires a client wallet.`, + }); + } + + return; + } + + if (!isMtlsPort(tnsServicePort)) { + return; + } + + if (!hasWalletFiles(configDir)) { + throw new Error( + `Mutual TLS requires wallet files (${WALLET_FILES.join(', ')}) in the TNS directory "${configDir}".`, + ); + } + + if (!walletPassword && !hasAutoLoginWallet(configDir)) { + throw new Error( + `Mutual TLS requires a wallet password (OCI wallet zip password), unless the directory contains an auto-login wallet (cwallet.sso).`, + ); + } + + if (!walletPassword && hasAutoLoginWallet(configDir)) { + logger({ + message: 'Using auto-login wallet (cwallet.sso); wallet password not required.', + }); + } +}; + +module.exports = { + WALLET_FILES, + MTLS_PORT, + hasWalletFiles, + isMutualTlsEnabled, + isMtlsPort, + connectStringUsesMtlsPort, + assertTnsMtlsRequirements, +}; From b5c5ba93bd374f81b99513535296026e946e2044 Mon Sep 17 00:00:00 2001 From: "ugo.bechameil" Date: Tue, 26 May 2026 22:37:53 +0200 Subject: [PATCH 7/7] make sonar happy --- .../connectionSettingsModalConfig.json | 13 ++-- reverse_engineering/helpers/connectionAuth.js | 66 +++++++++++++++++++ reverse_engineering/helpers/oracleHelper.js | 15 ++++- 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 reverse_engineering/helpers/connectionAuth.js diff --git a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json index b2763125..fcb31309 100644 --- a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json +++ b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json @@ -245,21 +245,24 @@ { "inputLabel": "Authentication method", "inputKeyword": "authMethod", + "description": "OS and Kerberos require Thick mode and a configured Oracle client (Instant Client or Oracle Home).", "inputType": "select", "defaultValue": "Username / Password", - "options": [{ "value": "Username / Password", "label": "Username / Password" }] + "options": [ + { "value": "Username / Password", "label": "Username / Password" }, + { "value": "OS", "label": "OS" }, + { "value": "Kerberos", "label": "Kerberos" } + ] }, { "inputLabel": "User Name", "inputKeyword": "userName", "inputType": "text", "inputPlaceholder": "User Name", + "description": "For Kerberos proxy connections, use the [username] format.", "dependency": { "key": "authMethod", "value": ["Username / Password", "Kerberos"] - }, - "validation": { - "regex": "([^\\s])" } }, { @@ -269,7 +272,7 @@ "inputPlaceholder": "Password", "dependency": { "key": "authMethod", - "value": ["Username / Password", "Kerberos"] + "value": ["Username / Password"] }, "isHiddenKey": true, "validation": { diff --git a/reverse_engineering/helpers/connectionAuth.js b/reverse_engineering/helpers/connectionAuth.js new file mode 100644 index 00000000..abd5bd30 --- /dev/null +++ b/reverse_engineering/helpers/connectionAuth.js @@ -0,0 +1,66 @@ +const _ = require('lodash'); +const { normalizeTnsAlias } = require('./tns/tnsConnectString'); + +const AUTH_METHOD_USERNAME_PASSWORD = 'Username / Password'; +const AUTH_METHOD_OS = 'OS'; +const AUTH_METHOD_KERBEROS = 'Kerberos'; + +const normalizeAuthMethod = authMethod => authMethod || AUTH_METHOD_USERNAME_PASSWORD; + +const assertExternalAuthMode = (authMethod, mode) => { + if (authMethod === AUTH_METHOD_USERNAME_PASSWORD) { + return; + } + + if (mode === 'thin') { + throw new Error( + `${authMethod} authentication requires Thick mode with Oracle Instant Client or Oracle Home configured for external authentication.`, + ); + } +}; + +const buildConnectionAuthParams = (authMethod, userName, userPassword) => { + if (authMethod === AUTH_METHOD_USERNAME_PASSWORD) { + if (!normalizeTnsAlias(userName) || !userPassword) { + throw new Error('User name and password are required for Username / Password authentication.'); + } + + return { username: userName, password: userPassword }; + } + + if (authMethod === AUTH_METHOD_OS) { + return { externalAuth: true }; + } + + if (authMethod === AUTH_METHOD_KERBEROS) { + const trimmedUserName = normalizeTnsAlias(userName); + const proxyUserName = + trimmedUserName && !trimmedUserName.startsWith('[') ? `[${trimmedUserName}]` : trimmedUserName; + + return _.omitBy( + { + externalAuth: true, + username: proxyUserName || undefined, + }, + _.isUndefined, + ); + } + + return { username: userName, password: userPassword }; +}; + +const logAuthMethodNotes = (authMethod, userPassword, logger) => { + if (authMethod === AUTH_METHOD_KERBEROS && userPassword) { + logger({ + message: + 'Password is not sent for Kerberos external authentication (oracledb uses the OS Kerberos ticket).', + }); + } +}; + +module.exports = { + normalizeAuthMethod, + assertExternalAuthMode, + buildConnectionAuthParams, + logAuthMethodNotes, +}; diff --git a/reverse_engineering/helpers/oracleHelper.js b/reverse_engineering/helpers/oracleHelper.js index b4da2676..29457c1a 100644 --- a/reverse_engineering/helpers/oracleHelper.js +++ b/reverse_engineering/helpers/oracleHelper.js @@ -8,6 +8,12 @@ const { getSchemaSynonyms } = require('./getSchemaSynonyms'); const { normalizeConnectString, getConnectionDescription } = require('./connectStringDescription'); const { clearPluginTnsAdmin } = require('./tns/tnsAdmin'); const { normalizeTnsAlias, getResolvedTnsService } = require('./tns/tnsConnectString'); +const { + normalizeAuthMethod, + assertExternalAuthMode, + buildConnectionAuthParams, + logAuthMethodNotes, +} = require('./connectionAuth'); const { trySyncTnsEndpointEarly, resolveConnectionConfigDir, @@ -235,6 +241,10 @@ const connect = async (connectionInfo, sshService, logger) => { }); logWalletConnectNotes({ connectionMethod, useMutualTls, useWallet, walletPassword }, logger); + const resolvedAuthMethod = normalizeAuthMethod(authMethod); + assertExternalAuthMode(resolvedAuthMethod, mode); + logAuthMethodNotes(resolvedAuthMethod, userPassword, logger); + const normalizedConnectString = normalizeConnectString(connectString); const hostnameToResolve = connectionMethod === 'Basic' ? host : connectionInfo.host; @@ -253,8 +263,7 @@ const connect = async (connectionInfo, sshService, logger) => { return authByCredentials({ connectString: normalizedConnectString, - username: userName, - password: userPassword, + ...buildConnectionAuthParams(resolvedAuthMethod, userName, userPassword), queryRequestTimeout, authRole, configDir: useWallet ? configDir : undefined, @@ -290,6 +299,7 @@ const authByCredentials = ({ connectString, username, password, + externalAuth, queryRequestTimeout, authRole, walletPassword, @@ -301,6 +311,7 @@ const authByCredentials = ({ { username, password, + externalAuth, connectString, privilege: authRole === 'default' ? undefined : oracleDB[authRole], walletLocation,