/*! * @license * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { AlfrescoApi, PeopleApi, NodesApi, GroupsApi, SitesApi, SearchApi } from '@alfresco/js-api'; import { argv, exit } from 'node:process'; import program from 'commander'; import { logger } from './logger'; interface PeopleTally { enabled: number; disabled: number; } interface RowToPrint { label: string; value: number; } const MAX_ATTEMPTS = 1; const TIMEOUT = 180000; const MAX_PEOPLE_PER_PAGE = 100; const USERS_HOME_RELATIVE_PATH = 'User Homes'; const reset = '\x1b[0m'; const grey = '\x1b[90m'; const cyan = '\x1b[36m'; const yellow = '\x1b[33m'; const bright = '\x1b[1m'; const red = '\x1b[31m'; const green = '\x1b[32m'; let alfrescoApi: AlfrescoApi; let loginAttempts: number = 0; /** * Scan environment command */ export default async function main() { // eslint-disable-next-line no-console console.log = () => {}; program .version('0.1.0') .option('--host ', 'Remote environment host') .option('--clientId [type]', 'sso client', 'alfresco') .option('-p, --password ', 'password ') .option('-u, --username ', 'username ') .parse(argv); logger.info(`${cyan}${bright}Initiating environment scan...${reset}`); await attemptLogin(); const rowsToPrint: Array = []; const peopleCount = await getPeopleCount(); rowsToPrint.push({ label: 'Active Users', value: peopleCount.enabled }); rowsToPrint.push({ label: 'Deactivated Users', value: peopleCount.disabled }); rowsToPrint.push({ label: `User's Home Folders`, value: await getHomeFoldersCount() }); rowsToPrint.push({ label: 'Groups', value: await getGroupsCount() }); rowsToPrint.push({ label: 'Sites', value: await getSitesCount() }); rowsToPrint.push({ label: 'Files', value: await getFilesCount() }); logger.info(generateTable(rowsToPrint)); } /** * Generate table * * @param rowsToPrint list of table rows to print * @returns table as a string */ function generateTable(rowsToPrint: Array) { const columnWidths = rowsToPrint.reduce( (maxWidths, row: RowToPrint) => ({ labelColumn: Math.max(maxWidths.labelColumn, row.label.length), valueColumn: Math.max(maxWidths.valueColumn, row.value.toString().length) }), { labelColumn: 12, valueColumn: 1 } ); const horizontalLine = ''.padEnd(columnWidths.labelColumn + columnWidths.valueColumn + 5, '═'); const headerText = 'ENVIRONM'.padStart(Math.floor((columnWidths.labelColumn + columnWidths.valueColumn + 3) / 2), ' ') + 'ENT SCAN'.padEnd(Math.ceil((columnWidths.labelColumn + columnWidths.valueColumn + 3) / 2), ' '); let tableString = `${grey}╒${horizontalLine}╕${reset} ${grey}│ ${bright}${cyan}${headerText} ${grey}│${reset} ${grey}╞${horizontalLine}╡${reset}`; rowsToPrint.forEach((row) => { tableString += `\n${grey}│${reset} ${row.label.padEnd(columnWidths.labelColumn, ' ')} ${yellow}${row.value .toString() .padEnd(columnWidths.valueColumn, ' ')} ${grey}│${reset}`; }); tableString += `\n${grey}╘${horizontalLine}╛${reset}`; return tableString; } /** * Attempt to login */ async function attemptLogin() { logger.info(` Logging into ${yellow}${program.host}${reset} with user ${yellow}${program.username}${reset}`); try { alfrescoApi = new AlfrescoApi({ provider: 'ALL', hostBpm: program.host, hostEcm: program.host, authType: 'OAUTH', contextRoot: 'alfresco', oauth2: { host: `${program.host}/auth/realms/alfresco`, clientId: `${program.clientId}`, scope: 'openid', redirectUri: '/', implicitFlow: false } }); await alfrescoApi.login(program.username, program.password); logger.info(` ${green}Login SSO successful${reset}`); } catch (err) { await handleLoginError(err); } } /** * Handles login error * * @param loginError error object */ async function handleLoginError(loginError) { if (loginAttempts === 0) { logger.error(` ${red}Login SSO error${reset}`); } checkEnvReachable(loginError); loginAttempts++; if (MAX_ATTEMPTS === loginAttempts) { if (loginError?.response?.text) { try { const parsedJson = JSON.parse(loginError?.response?.text); if (typeof parsedJson === 'object' && parsedJson.error) { const { stackTrace, ...errorWithoutDeprecatedProperty } = parsedJson.error; logger.error(errorWithoutDeprecatedProperty); } } catch (jsonParseError) { logger.error(` ${red}Could not parse the error response. Possibly non json format${reset}`); } } logger.error(` ${red}Give up${reset}`); failScript(); } else { logger.error(` Retry in 1 minute attempt N ${loginAttempts}`); await wait(TIMEOUT); await attemptLogin(); } } /** * Check environment is reachable * * @param loginError error object */ function checkEnvReachable(loginError) { const failingErrorCodes = ['ENOTFOUND', 'ETIMEDOUT', 'ECONNREFUSED']; if (typeof loginError === 'object' && failingErrorCodes.indexOf(loginError.code) > -1) { logger.error(` ${red}The environment is not reachable (${loginError.code})${reset}`); failScript(); } } /** * Get people count * * @param skipCount skip count */ async function getPeopleCount(skipCount: number = 0): Promise { if (skipCount === 0) { logger.info(` Fetching number of users`); } try { const peopleApi = new PeopleApi(alfrescoApi); const apiResult = await peopleApi.listPeople({ fields: ['enabled'], maxItems: MAX_PEOPLE_PER_PAGE, skipCount }); const result: PeopleTally = apiResult.list.entries.reduce( (peopleTally: PeopleTally, currentPerson) => { if (currentPerson.entry.enabled) { peopleTally.enabled++; } else { peopleTally.disabled++; } return peopleTally; }, { enabled: 0, disabled: 0 } ); if (apiResult.list.pagination.hasMoreItems) { const more = await getPeopleCount(apiResult.list.pagination.skipCount + MAX_PEOPLE_PER_PAGE); result.enabled += more.enabled; result.disabled += more.disabled; } return result; } catch (error) { handleError(error); } } /** * Count the amount of Home folders */ async function getHomeFoldersCount(): Promise { logger.info(` Fetching number of home folders`); try { const nodesApi = new NodesApi(alfrescoApi); const homesFolderApiResult = await nodesApi.listNodeChildren('-root-', { maxItems: 1, relativePath: USERS_HOME_RELATIVE_PATH }); return homesFolderApiResult.list.pagination.totalItems; } catch (error) { handleError(error); } } /** * Count the amount of groups */ async function getGroupsCount(): Promise { logger.info(` Fetching number of groups`); try { const groupsApi = new GroupsApi(alfrescoApi); const groupsApiResult = await groupsApi.listGroups({ maxItems: 1 }); return groupsApiResult.list.pagination.totalItems; } catch (error) { handleError(error); } } /** * Count the amount of sites */ async function getSitesCount(): Promise { logger.info(` Fetching number of sites`); try { const sitesApi = new SitesApi(alfrescoApi); const sitesApiResult = await sitesApi.listSites({ maxItems: 1 }); return sitesApiResult.list.pagination.totalItems; } catch (error) { handleError(error); } } /** * Count the amount of files */ async function getFilesCount(): Promise { logger.info(` Fetching number of files`); try { const searchApi = new SearchApi(alfrescoApi); const searchApiResult = await searchApi.search({ query: { query: 'select * from cmis:document', language: 'cmis' }, paging: { maxItems: 1 } }); return searchApiResult.list.pagination.totalItems; } catch (error) { handleError(error); } } /** * Handle error * * @param error error object */ function handleError(error) { logger.error(` ${red}Error encountered${reset}`); if (error?.response && error?.response?.text) { try { const parsedJson = JSON.parse(error?.response?.text); if (typeof parsedJson === 'object' && parsedJson.error) { const { stackTrace, ...errorWithoutDeprecatedProperty } = parsedJson.error; logger.error(errorWithoutDeprecatedProperty); } } catch (jsonParseError) { logger.error(` ${red}Could not parse the error response. Possibly non json format${reset}`); } } failScript(); } /** * Log the error and exit */ function failScript() { logger.error(`${red}${bright}Environment scan failed. Exiting${reset}`); exit(0); } /** * Wait with a certain period * * @param ms timeout in milliseconds */ async function wait(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }