From 63178ac31a69c75c4a938161fa85614aa7b62420 Mon Sep 17 00:00:00 2001 From: Nazar Kovtun <88377450+WilhelmWesser@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:05:01 +0200 Subject: [PATCH] Hck 8554 display a proper error in case a user needs to consent hackolade azure ad app to allow access his resources (#80) * HCK-8554: extended errors processing to handle Entra grant consent issues * HCK-8554: changed error processing service location * HCk-8554: removed redundant variables and escape characters --------- Co-authored-by: Vitalii Yarmus <71256742+Vitalii4as@users.noreply.github.com> --- reverse_engineering/api.js | 23 ++++- .../databaseService/helpers/errorService.js | 99 +++++++++++++++++++ 2 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 reverse_engineering/databaseService/helpers/errorService.js diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index bd46695..31c3eaa 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -15,6 +15,7 @@ const getAdditionalAccountInfo = require('./helpers/getAdditionalAccountInfo'); const crypto = require('crypto'); const randomstring = require('randomstring'); const base64url = require('base64url'); +const { prepareError } = require('./databaseService/helpers/errorService'); module.exports = { async connect(connectionInfo, logger, callback, app) { @@ -42,8 +43,17 @@ module.exports = { } callback(null); } catch (error) { - logger.log('error', { message: error.message, stack: error.stack, error }, 'Test connection'); - callback({ message: error.message, stack: error.stack }); + const errorWithUpdatedInfo = prepareError({ error }); + logger.log( + 'error', + { + message: errorWithUpdatedInfo.message, + stack: errorWithUpdatedInfo.stack, + error: errorWithUpdatedInfo, + }, + 'Test connection', + ); + callback({ message: errorWithUpdatedInfo.message, stack: errorWithUpdatedInfo.stack }); } }, @@ -82,12 +92,17 @@ module.exports = { const objects = await getObjectsFromDatabase(client); callback(null, objects); } catch (error) { + const errorWithUpdatedInfo = prepareError({ error }); logger.log( 'error', - { message: error.message, stack: error.stack, error }, + { + message: errorWithUpdatedInfo.message, + stack: errorWithUpdatedInfo.stack, + error: errorWithUpdatedInfo, + }, 'Retrieving databases and tables information', ); - callback({ message: error.message, stack: error.stack }); + callback({ message: errorWithUpdatedInfo.message, stack: errorWithUpdatedInfo.stack }); } }, diff --git a/reverse_engineering/databaseService/helpers/errorService.js b/reverse_engineering/databaseService/helpers/errorService.js new file mode 100644 index 0000000..cb18d5e --- /dev/null +++ b/reverse_engineering/databaseService/helpers/errorService.js @@ -0,0 +1,99 @@ +/** + * + * @param {{message: string}} param + * @returns {boolean} + */ +const isDisabledPublicClientFlowsError = ({ message }) => { + const DISABLED_PUBLIC_CLIENT_FLOWS_ERROR_ID = 'AADSTS7000218'; + + return message.includes(DISABLED_PUBLIC_CLIENT_FLOWS_ERROR_ID); +}; + +/** + * + * @param {{message: string}} param + * @returns {boolean} + */ +const isConsentRequiredError = ({ message }) => { + const CONSENT_REQUIRED_ERROR_ID = 'AADSTS65001'; + + return message.includes(CONSENT_REQUIRED_ERROR_ID); +}; + +/** + * + * @param {{error: object, newMessage: string, newStackTrace: string}} param + * @returns {object} + */ +const updateErrorMessageAndStack = ({ error, newMessage, newStackTrace }) => ({ + code: error.code, + name: error.name, + message: newMessage, + stack: newStackTrace, +}); + +/** + * + * @param {{clientId: string}} param + * @returns {string} + */ +const getConsentRequiredErrorMessage = ({ clientId }) => { + const consentLink = `https://login.microsoftonline.com/organizations/adminconsent?client_id=${clientId}`; + + return `Your Azure administrator needs to grant tenant-wide consent to the Hackolade application using the link below: ${consentLink}`; +}; + +/** + * + * @param {{match: string}} param + * @returns {string} + */ +const getClientIdFromErrorMessage = ({ message }) => { + const clientIdRegularExpression = new RegExp(/'[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}'/gim); + const clientIdMatches = message.match(clientIdRegularExpression); + + if (clientIdMatches.length === 0) { + return 'Unknown'; + } + + const [clientId] = clientIdMatches; + const clientIdWithoutQuotes = clientId.slice(1, clientId.length - 1); + + return clientIdWithoutQuotes; +}; + +/** + * + * @param {{error: object}} param + * @returns {object} + */ +const prepareError = ({ error }) => { + const originalErrors = error?.originalError?.errors; + if (!originalErrors || originalErrors?.length === 0) { + return error; + } + + const initialErrorDataIndex = originalErrors.length - 1; + const initialError = originalErrors[initialErrorDataIndex]; + + const isInitialErrorConsentRequiredError = isConsentRequiredError(initialError); + if (isInitialErrorConsentRequiredError) { + const clientId = getClientIdFromErrorMessage({ message: initialError.message }); + const newErrorMessage = getConsentRequiredErrorMessage({ clientId }); + + return updateErrorMessageAndStack({ error, newMessage: newErrorMessage, newStackTrace: initialError.stack }); + } + + const isInitialErrorDisabledPublicClientFlowsError = isDisabledPublicClientFlowsError(initialError); + if (isInitialErrorDisabledPublicClientFlowsError) { + const newErrorMessage = 'You need to allow Public client flows for the Entra ID application'; + + return updateErrorMessageAndStack({ error, newMessage: newErrorMessage, newStackTrace: initialError.stack }); + } + + return error; +}; + +module.exports = { + prepareError, +};