Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions forward_engineering/api/testConnection.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
const { logInfo } = require('../../reverse_engineering/helpers/logInfo');
const { shouldUseExternalBrowser } = require('../../reverse_engineering/helpers/mfaAccessToken');
const { getExternalBrowserUrl, connect } = require('../../reverse_engineering/api');

async function testConnection(connectionInfo, logger, callback, app) {
try {
logInfo('Test connection', connectionInfo, logger);
if (connectionInfo.authMethod === 'Azure Active Directory (MFA)') {
if (shouldUseExternalBrowser({ app, connectionInfo })) {
await getExternalBrowserUrl(connectionInfo, logger, callback);
} else {
await connect(connectionInfo, logger);
await connect(connectionInfo, logger, undefined, app);
}
callback(null);
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion forward_engineering/helpers/applyToInstanceHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const applyToInstance = async (connectionInfo, logger, app) => {
const async = app.require('async');

try {
const client = await connect(connectionInfo, logger);
const client = await connect(connectionInfo, logger, undefined, app);
if (!client.config.database) {
throw new Error('No database specified');
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Synapse",
"version": "0.2.35",
"version": "0.2.36",
"author": "hackolade",
"engines": {
"hackolade": "6.1.2",
Expand Down Expand Up @@ -80,4 +80,4 @@
"uuid": "14.0.0",
"brace-expansion": "5.0.12"
}
}
}
21 changes: 14 additions & 7 deletions reverse_engineering/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const {
getCollectionsRelationships,
} = require('./reverseEngineeringService/reverseEngineeringService');
const { logInfo } = require('./helpers/logInfo');
const { canRequestUserToMachineAccessToken, shouldUseExternalBrowser } = require('./helpers/mfaAccessToken');
const { getEntraOAuthConfig, resolveAuthorizeTenant } = require('./helpers/entraOAuthConfig');
const filterRelationships = require('./helpers/filterRelationships');
const getOptionsFromConnectionInfo = require('./helpers/getOptionsFromConnectionInfo');
const getAdditionalAccountInfo = require('./helpers/getAdditionalAccountInfo');
Expand All @@ -22,7 +24,7 @@ module.exports = {
async connect(connectionInfo, logger, callback, app) {
const client = getClient();
if (!client) {
await setClient(connectionInfo, logger);
await setClient(connectionInfo, logger, app);
return getClient();
}

Expand All @@ -37,10 +39,10 @@ module.exports = {
async testConnection(connectionInfo, logger, callback, app) {
try {
logInfo('Test connection', connectionInfo, logger);
if (connectionInfo.authMethod === 'Azure Active Directory (MFA)') {
if (shouldUseExternalBrowser({ app, connectionInfo })) {
await this.getExternalBrowserUrl(connectionInfo, logger, callback);
} else {
await this.connect(connectionInfo, logger);
await this.connect(connectionInfo, logger, undefined, app);
}
callback(null);
} catch (error) {
Expand All @@ -59,18 +61,23 @@ module.exports = {
},

async getExternalBrowserUrl(connectionInfo, logger, cb, app) {
if (canRequestUserToMachineAccessToken({ app, connectionInfo })) {
cb(null, {});
return;
}

logInfo('Get external browser URL', connectionInfo, logger);
const verifier = randomstring.generate(32);
const base64Digest = crypto.createHash('sha256').update(verifier).digest('base64');
const challenge = base64url.fromBase64(base64Digest);
const tenantId = connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common';
const clientId = '0dc36597-bc44-49f8-a4a7-ae5401959b85';
const tenantId = resolveAuthorizeTenant({ connectionInfo });
const { productionClientId, authorizationEndpoint } = await getEntraOAuthConfig({ app, tenantId });
const loginHint = connectionInfo.loginHint ? `login_hint=${encodeURIComponent(connectionInfo.loginHint)}&` : '';
const redirectUrl = `http://localhost:${connectionInfo.redirectPort || 8080}`;

cb(null, {
proofKey: verifier,
url: `https://login.microsoftonline.com/${tenantId}/oauth2/authorize?${loginHint}code_challenge_method=S256&code_challenge=${challenge}&response_type=code&response_mode=query&client_id=${clientId}&redirect_uri=${redirectUrl}&prompt=select_account&resource=https://database.windows.net/`,
url: `${authorizationEndpoint}?${loginHint}code_challenge_method=S256&code_challenge=${challenge}&response_type=code&response_mode=query&client_id=${productionClientId}&redirect_uri=${redirectUrl}&prompt=select_account&resource=https://database.windows.net/`,
});
},

Expand All @@ -85,7 +92,7 @@ module.exports = {
async getDbCollectionsNames(connectionInfo, logger, callback, app) {
try {
logInfo('Retrieving databases and tables information', connectionInfo, logger);
const client = await this.connect(connectionInfo, logger);
const client = await this.connect(connectionInfo, logger, undefined, app);
if (!client.config.database) {
throw new Error('No database specified');
}
Expand Down
4 changes: 2 additions & 2 deletions reverse_engineering/connectionState.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ module.exports = {
_client: null,
_connectionInfo: {},
getClient: () => this._client,
setClient: async (connectionInfo, logger) => {
setClient: async (connectionInfo, logger, app) => {
this._connectionInfo = connectionInfo;
this._client = await getConnectionClient(connectionInfo, logger);
this._client = await getConnectionClient(connectionInfo, logger, app);
},
getConnectionInfo: () => this._connectionInfo,
clearClient: () => (this._client = null),
Expand Down
11 changes: 8 additions & 3 deletions reverse_engineering/databaseService/databaseService.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { getObjectsFromDatabase, getNewConnectionClientByDb } = require('./helper
const getSampleDocSize = require('../helpers/getSampleDocSize');
const { logAuthTokenInfo } = require('../helpers/logInfo');
const { getConnection } = require('./helpers/connection');
const { getEntraOAuthConfig, resolveAuthorizeTenant } = require('../helpers/entraOAuthConfig');
const { progress } = require('../helpers/logInfo');
const {
getPartitionsSubQueryForRetrievingTheTablesSelectedByTheUser,
Expand All @@ -15,13 +16,13 @@ const { parseProcedure } = require('./helpers/parsers/parseProcedure');

const QUERY_REQUEST_TIMEOUT = 60000;

const getConnectionClient = async (connectionInfo, logger) => {
const getConnectionClient = async (connectionInfo, logger, app) => {
const hostName = getHostName(connectionInfo.host);
const userName =
isEmail(connectionInfo.userName) && hostName
? `${connectionInfo.userName}@${hostName}`
: connectionInfo.userName;
const tenantId = connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common';
const tenantId = resolveAuthorizeTenant({ connectionInfo });
const queryRequestTimeout = Number(connectionInfo.queryRequestTimeout) || QUERY_REQUEST_TIMEOUT;

const commonConfig = {
Expand All @@ -36,7 +37,10 @@ const getConnectionClient = async (connectionInfo, logger) => {
password: connectionInfo.userPassword,
};

const clientId = '0dc36597-bc44-49f8-a4a7-ae5401959b85';
const clientId =
connectionInfo.authMethod === 'Azure Active Directory (Username / Password)'
? (await getEntraOAuthConfig({ app })).productionClientId
: undefined;
const redirectUri = 'http://localhost:8080';

const connection = getConnection({
Expand All @@ -48,6 +52,7 @@ const getConnectionClient = async (connectionInfo, logger) => {
clientId,
redirectUri,
logger,
app,
});

return connection.connect();
Expand Down
49 changes: 8 additions & 41 deletions reverse_engineering/databaseService/helpers/connection.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const { hckFetch } = require('@hackolade/fetch');
const sql = require('mssql');
const { logAuthTokenInfo, logConnectionHostAndUsername } = require('../../helpers/logInfo');
const { prepareError } = require('./errorService');
const { parseResponse } = require('../../helpers/parseResponse');
const { getMfaAccessToken } = require('../../helpers/mfaAccessToken');

class Connection {
constructor({ logger }) {
Expand Down Expand Up @@ -51,17 +49,19 @@ class UsernamePasswordConnection extends Connection {
}

class AzureActiveDirectoryMFAConnection extends Connection {
constructor({ connectionInfo, commonConfig, tenantId, clientId, redirectUri, logger }) {
constructor({ connectionInfo, commonConfig, logger, app }) {
super({ logger });
this.connectionInfo = connectionInfo;
this.commonConfig = commonConfig;
this.tenantId = tenantId;
this.clientId = clientId;
this.redirectUri = redirectUri;
this.app = app;
}

async connect() {
const token = await this.#getToken();
const token = await getMfaAccessToken({
app: this.app,
connectionInfo: this.connectionInfo,
logger: this.logger,
});
logAuthTokenInfo({ token, logger: this.logger });
logConnectionHostAndUsername({ authMethod: this.connectionInfo.authMethod, logger: this.logger });
return sql.connect({
Expand All @@ -78,39 +78,6 @@ class AzureActiveDirectoryMFAConnection extends Connection {
},
});
}

async #getToken() {
try {
const urlParams = new URLSearchParams();

urlParams.append('code', this.connectionInfo?.externalBrowserQuery?.code || '');
urlParams.append('client_id', this.clientId);
urlParams.append('redirect_uri', this.redirectUri);
urlParams.append('grant_type', 'authorization_code');
urlParams.append('code_verifier', this.connectionInfo?.proofKey);

const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'http://localhost',
},
body: urlParams,
};

const response = await hckFetch(
`https://login.microsoftonline.com/organizations/oauth2/v2.0/token`,
options,
);
const responseData = await parseResponse(response);

return responseData?.access_token || '';
} catch (error) {
this.logger.log('error', { message: error.message, stack: error.stack, error }, 'MFA auth error');
return '';
}
}
}

class AzureActiveDirectoryUsernamePasswordConnection extends Connection {
Expand Down
48 changes: 48 additions & 0 deletions reverse_engineering/helpers/entraOAuthConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

const OAUTH_MODULE = '@hackolade/oauth';
const OAUTH_NOT_EXPOSED_MESSAGE = `The module ${OAUTH_MODULE} is not exposed by the application to the plugins!`;
const FALLBACK_PRODUCTION_CLIENT_ID = '0dc36597-bc44-49f8-a4a7-ae5401959b85';
const FALLBACK_TOKEN_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token';
const FALLBACK_AUTHORIZE_ENDPOINT = 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize';
const FALLBACK_DESKTOP_TOKEN_REQUEST_HEADERS = { Origin: 'http://localhost' };

const loadOauthModule = ({ app } = {}) => {
try {
return app.require(OAUTH_MODULE);
} catch (error) {
if (error?.message === OAUTH_NOT_EXPOSED_MESSAGE) {
return null;
}

throw error;
}
};

const resolveAuthorizeTenant = ({ connectionInfo = {} } = {}) =>
connectionInfo.connectionTenantId || connectionInfo.tenantId || 'common';

const getLegacyEntraOAuthConfig = ({ tenantId } = {}) => ({
productionClientId: FALLBACK_PRODUCTION_CLIENT_ID,
authorizationEndpoint: tenantId
? `https://login.microsoftonline.com/${tenantId}/oauth2/authorize`
: FALLBACK_AUTHORIZE_ENDPOINT,
tokenEndpoint: FALLBACK_TOKEN_ENDPOINT,
desktopTokenRequestHeaders: FALLBACK_DESKTOP_TOKEN_REQUEST_HEADERS,
});

const getEntraOAuthConfig = async ({ app, tenantId } = {}) => {
const oauth = loadOauthModule({ app });

if (!oauth) {
return getLegacyEntraOAuthConfig({ tenantId });
}

return oauth.getEntraIdOAuthConfig(tenantId === undefined ? {} : { tenantId });
};

module.exports = {
loadOauthModule,
resolveAuthorizeTenant,
getEntraOAuthConfig,
};
Loading
Loading