diff --git a/reverse_engineering/databaseService/databaseService.js b/reverse_engineering/databaseService/databaseService.js index 140435b..b7bba22 100644 --- a/reverse_engineering/databaseService/databaseService.js +++ b/reverse_engineering/databaseService/databaseService.js @@ -5,6 +5,7 @@ const { getObjectsFromDatabase, getNewConnectionClientByDb } = require('./helper const msal = require('@azure/msal-node'); const getSampleDocSize = require('../helpers/getSampleDocSize'); const { logAuthTokenInfo } = require('../helpers/logInfo'); +const { getConnection } = require('./helpers/connection'); const QUERY_REQUEST_TIMEOUT = 60000; @@ -17,12 +18,6 @@ const getConnectionClient = async (connectionInfo, logger) => { const tenantId = connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common'; const queryRequestTimeout = Number(connectionInfo.queryRequestTimeout) || QUERY_REQUEST_TIMEOUT; - logger.log( - 'info', - `hostname: ${hostName}, username: ${userName}, auth method: ${connectionInfo.authMethod}`, - 'Auth info', - ); - const commonConfig = { server: connectionInfo.host, port: +connectionInfo.port, @@ -38,63 +33,20 @@ const getConnectionClient = async (connectionInfo, logger) => { const clientId = '0dc36597-bc44-49f8-a4a7-ae5401959b85'; const redirectUri = 'http://localhost:8080'; - switch (connectionInfo.authMethod) { - case 'Username / Password': - return sql.connect({ - ...commonConfig, - ...credentialsConfig, - options: { - encrypt: true, - enableArithAbort: true, - }, - }); - case 'Username / Password (Windows)': - return sql.connect({ - ...commonConfig, - ...credentialsConfig, - domain: connectionInfo.userDomain, - options: { - encrypt: false, - enableArithAbort: true, - }, - }); - case 'Azure Active Directory (MFA)': - const token = await getToken({ connectionInfo, tenantId, clientId, redirectUri, logger }); - logAuthTokenInfo({ token, logger }); - return sql.connect({ - ...commonConfig, - options: { - encrypt: true, - enableArithAbort: true, - }, - authentication: { - type: 'azure-active-directory-access-token', - options: { - token, - }, - }, - }); - case 'Azure Active Directory (Username / Password)': - return sql.connect({ - ...commonConfig, - ...credentialsConfig, - options: { - encrypt: true, - enableArithAbort: true, - }, - authentication: { - type: 'azure-active-directory-password', - options: { - userName: connectionInfo.userName, - password: connectionInfo.userPassword, - tenantId, - clientId, - }, - }, - }); - } + const connection = getConnection({ + type: connectionInfo.authMethod, + data: { + connectionInfo, + commonConfig, + credentialsConfig, + tenantId, + clientId, + redirectUri, + logger, + }, + }); - return await sql.connect(connectionInfo.connectionString); + return connection.connect(); }; const isEmail = name => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(name || ''); @@ -513,98 +465,13 @@ const mapResponse = async (response = {}) => { return (await response).recordset; }; -const getTokenByMSAL = async ({ connectionInfo, redirectUri, clientId, tenantId, logger }) => { - try { - const pca = new msal.PublicClientApplication(getAuthConfig(clientId, tenantId, logger.log)); - const tokenRequest = { - code: connectionInfo?.externalBrowserQuery?.code || '', - scopes: ['https://database.windows.net//.default'], - redirectUri, - codeVerifier: connectionInfo?.proofKey, - clientInfo: connectionInfo?.externalBrowserQuery?.client_info || '', - }; - - const responseData = await pca.acquireTokenByCode(tokenRequest); - - return responseData.accessToken; - } catch (error) { - logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA MSAL auth error'); - return ''; - } -}; - -const getAgent = (reject, cert, key) => { - return new https.Agent({ cert, key, rejectUnauthorized: !!reject }); -}; - -const getTokenByAxios = async ({ connectionInfo, tenantId, redirectUri, clientId, logger, agent }) => { - try { - const params = new URLSearchParams(); - params.append('code', connectionInfo?.externalBrowserQuery?.code || ''); - params.append('client_id', clientId); - params.append('redirect_uri', redirectUri); - params.append('grant_type', 'authorization_code'); - params.append('code_verifier', connectionInfo?.proofKey); - params.append('resource', 'https://database.windows.net/'); - - const responseData = await axios.post(`https://login.microsoftonline.com/${tenantId}/oauth2/token`, params, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - ...(agent && { httpsAgent: agent }), - }); - - return responseData?.data?.access_token || ''; - } catch (error) { - logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA Axios auth error'); - return ''; - } -}; - -const getTokenByAxiosExtended = params => { - return getTokenByAxios({ ...params, agent: getAgent() }); -}; - -const getToken = async ({ connectionInfo, tenantId, clientId, redirectUri, logger }) => { - const axiosExtendedToken = await getTokenByAxiosExtended({ - connectionInfo, - clientId, - redirectUri, - tenantId, - logger, - }); - if (axiosExtendedToken) { - return axiosExtendedToken; - } - - const msalToken = await getTokenByMSAL({ connectionInfo, clientId, redirectUri, tenantId, logger }); - if (msalToken) { - return msalToken; - } - - const axiosToken = await getTokenByAxios({ connectionInfo, clientId, redirectUri, tenantId, logger }); - if (axiosToken) { - return axiosToken; - } - - return; -}; +async function getTableRowCount(tableSchema, tableName, currentDbConnectionClient) { + const rowCountQuery = `SELECT COUNT(*) as rowsCount FROM [${tableSchema}].[${tableName}]`; + const rowCountResponse = await currentDbConnectionClient.query(rowCountQuery); + const rowCount = rowCountResponse?.recordset[0]?.rowsCount; -const getAuthConfig = (clientId, tenantId, logger) => ({ - system: { - loggerOptions: { - loggerCallback(loglevel, message) { - logger(message); - }, - piiLoggingEnabled: false, - logLevel: msal.LogLevel.Verbose, - }, - }, - auth: { - clientId, - authority: `https://login.microsoftonline.com/${tenantId}`, - }, -}); + return rowCount; +} module.exports = { getConnectionClient, @@ -629,11 +496,3 @@ module.exports = { queryDistribution, getPartitions, }; - -async function getTableRowCount(tableSchema, tableName, currentDbConnectionClient) { - const rowCountQuery = `SELECT COUNT(*) as rowsCount FROM [${tableSchema}].[${tableName}]`; - const rowCountResponse = await currentDbConnectionClient.query(rowCountQuery); - const rowCount = rowCountResponse?.recordset[0]?.rowsCount; - - return rowCount; -} diff --git a/reverse_engineering/databaseService/helpers/connection.js b/reverse_engineering/databaseService/helpers/connection.js new file mode 100644 index 0000000..6c6b684 --- /dev/null +++ b/reverse_engineering/databaseService/helpers/connection.js @@ -0,0 +1,263 @@ +const axios = require('axios'); +const sql = require('mssql'); +const https = require('https'); +const msal = require('@azure/msal-node'); +const { logAuthTokenInfo } = require('../../helpers/logInfo'); +const { prepareError } = require('./errorService'); + +const logConnectionHostAndUsername = ({ hostname, username, authMethod, logger }) => { + logger.log( + 'info', + `hostname: ${hostname ?? 'absent'}, username: ${username ?? 'absent'}, auth method: ${authMethod}`, + 'Auth info', + ); +}; + +class Connection { + constructor({ logger }) { + this.logger = logger; + } + + async connect() {} +} + +class ConnectionStringConnection extends Connection { + constructor({ connectionInfo, logger }) { + super({ logger }); + this.connectionInfo = connectionInfo; + } + + async connect() { + logConnectionHostAndUsername({ authMethod: this.connectionInfo.authMethod, logger: this.logger }); + return sql.connect(this.connectionInfo.connectionString); + } +} + +class UsernamePasswordConnection extends Connection { + constructor({ commonConfig, credentialsConfig, logger }) { + super({ logger }); + this.commonConfig = commonConfig; + this.credentialsConfig = credentialsConfig; + } + + async connect() { + logConnectionHostAndUsername({ + hostname: this.commonConfig.hostName, + username: this.credentialsConfig.user, + authMethod: this.connectionInfo.authMethod, + logger: this.logger, + }); + return sql.connect({ + ...this.commonConfig, + ...this.credentialsConfig, + options: { + encrypt: true, + enableArithAbort: true, + }, + }); + } +} + +class UsernamePasswordWindowsConnection extends Connection { + constructor({ connectionInfo, commonConfig, credentialsConfig, logger }) { + super({ logger }); + this.connectionInfo = connectionInfo; + this.commonConfig = commonConfig; + this.credentialsConfig = credentialsConfig; + } + + async connect() { + logConnectionHostAndUsername({ + hostname: this.commonConfig.hostName, + username: this.credentialsConfig.user, + authMethod: this.connectionInfo.authMethod, + logger: this.logger, + }); + return sql.connect({ + ...this.commonConfig, + ...this.credentialsConfig, + domain: this.connectionInfo.userDomain, + options: { + encrypt: false, + enableArithAbort: true, + }, + }); + } +} + +class AzureActiveDirectoryMFAConnection extends Connection { + constructor({ connectionInfo, commonConfig, tenantId, clientId, redirectUri, logger }) { + super({ logger }); + this.connectionInfo = connectionInfo; + this.commonConfig = commonConfig; + this.tenantId = tenantId; + this.clientId = clientId; + this.redirectUri = redirectUri; + } + + async connect() { + const token = await this.#getToken(); + logAuthTokenInfo({ token, logger: this.logger }); + logConnectionHostAndUsername({ authMethod: this.connectionInfo.authMethod, logger: this.logger }); + return sql.connect({ + ...this.commonConfig, + options: { + encrypt: true, + enableArithAbort: true, + }, + authentication: { + type: 'azure-active-directory-access-token', + options: { + token, + }, + }, + }); + } + + async #getToken() { + const axiosExtendedToken = await this.#getTokenByAxiosExtended(); + if (axiosExtendedToken) { + return axiosExtendedToken; + } + + const msalToken = await this.#getTokenByMSAL(); + if (msalToken) { + return msalToken; + } + + const axiosToken = await this.#getTokenByAxios(); + if (axiosToken) { + return axiosToken; + } + } + + #getTokenByAxiosExtended() { + return this.#getTokenByAxios({ agent: this.#getAgent() }); + } + + #getAgent(reject, cert, key) { + return new https.Agent({ cert, key, rejectUnauthorized: Boolean(reject) }); + } + + async #getTokenByAxios({ agent }) { + try { + const params = new URLSearchParams(); + params.append('code', this.connectionInfo?.externalBrowserQuery?.code || ''); + params.append('client_id', this.clientId); + params.append('redirect_uri', this.redirectUri); + params.append('grant_type', 'authorization_code'); + params.append('code_verifier', this.connectionInfo?.proofKey); + params.append('resource', 'https://database.windows.net/'); + + const responseData = await axios.post( + `https://login.microsoftonline.com/${this.tenantId}/oauth2/token`, + params, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + ...(agent && { httpsAgent: agent }), + }, + ); + + return responseData?.data?.access_token || ''; + } catch (error) { + this.logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA Axios auth error'); + return ''; + } + } + + async #getTokenByMSAL() { + try { + const pca = new msal.PublicClientApplication(this.#getAuthConfig()); + const tokenRequest = { + code: this.connectionInfo?.externalBrowserQuery?.code || '', + scopes: ['https://database.windows.net//.default'], + redirectUri: this.redirectUri, + codeVerifier: this.connectionInfo?.proofKey, + clientInfo: this.connectionInfo?.externalBrowserQuery?.client_info || '', + }; + + const responseData = await pca.acquireTokenByCode(tokenRequest); + + return responseData.accessToken; + } catch (error) { + this.logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA MSAL auth error'); + return ''; + } + } + + async #getAuthConfig() { + return { + system: { + loggerOptions: { + loggerCallback(loglevel, message) { + this.logger.log(message); + }, + piiLoggingEnabled: false, + logLevel: msal.LogLevel.Verbose, + }, + }, + auth: { + clientId: this.clientId, + authority: `https://login.microsoftonline.com/${this.tenantId}`, + }, + }; + } +} + +class AzureActiveDirectoryUsernamePasswordConnection extends Connection { + constructor({ connectionInfo, commonConfig, credentialsConfig, tenantId, clientId, logger }) { + super({ logger }); + this.connectionInfo = connectionInfo; + this.commonConfig = commonConfig; + this.credentialsConfig = credentialsConfig; + this.tenantId = tenantId; + this.clientId = clientId; + } + + async connect() { + logConnectionHostAndUsername({ + hostname: this.commonConfig.hostName, + username: this.connectionInfo.userName, + authMethod: this.connectionInfo.authMethod, + logger: this.logger, + }); + return sql.connect({ + ...this.commonConfig, + ...this.credentialsConfig, + options: { + encrypt: true, + enableArithAbort: true, + }, + authentication: { + type: 'azure-active-directory-password', + options: { + userName: this.connectionInfo.userName, + password: this.connectionInfo.userPassword, + tenantId: this.tenantId, + clientId: this.clientId, + }, + }, + }); + } +} + +const getConnection = ({ type, data }) => { + switch (type) { + case 'Username / Password': + return new UsernamePasswordConnection(data); + case 'Username / Password (Windows)': + return new UsernamePasswordWindowsConnection(data); + case 'Azure Active Directory (MFA)': + return new AzureActiveDirectoryMFAConnection(data); + case 'Azure Active Directory (Username / Password)': + return new AzureActiveDirectoryUsernamePasswordConnection(data); + default: + return new ConnectionStringConnection(data); + } +}; + +module.exports = { + getConnection, +};