UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

213 lines (206 loc) • 12 kB
/* jscpd:ignore-start */ import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import c from 'chalk'; import { generateReports, isCI, uxLog, uxLogTable } from '../../../../common/utils/index.js'; import { promptProfiles } from '../../../../common/utils/orgUtils.js'; //import { executeApex } from "../../../../common/utils/deployUtils.js"; import { prompts } from '../../../../common/utils/prompts.js'; import { soqlQuery, bulkQuery, bulkUpdate } from '../../../../common/utils/apiUtils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class OrgFreezeUser extends SfCommand { static title = 'Freeze user logins'; static description = ` ## Command Behavior **Freezes Salesforce user logins, temporarily revoking access for selected users.** This command allows administrators to freeze Salesforce user logins. It provides a controlled way to temporarily revoke user access without deactivating the user record itself. This is useful for managing user access during leaves, security incidents, or when a user's access needs to be temporarily suspended. Key functionalities: - **User Selection:** You can select users to freeze based on their assigned profiles. - \`--includeprofiles\`: Freeze users belonging to a comma-separated list of specified profiles. - \`--excludeprofiles\`: Freeze users belonging to all profiles *except* those specified in a comma-separated list. - If no profile flags are provided, an interactive menu will allow you to select profiles. - **Interactive Confirmation:** In non-CI environments, it prompts for confirmation before freezing the selected users. - **Bulk Freezing:** Efficiently freezes multiple user logins using Salesforce's Bulk API. - **Reporting:** Generates CSV and XLSX reports of the users that are about to be frozen. <details markdown="1"> <summary>Technical explanations</summary> The command's technical implementation involves: - **SOQL Queries (Bulk API):** It executes SOQL queries against the \`User\` and \`Profile\` objects to identify active users based on the provided profile filters. It then queries the \`UserLogin\` object to find active login sessions for these users. - **Interactive Prompts:** Uses the \`prompts\` library to guide the user through profile selection and to confirm the freezing operation. - **Bulk Update:** It constructs an array of \`UserLogin\` records with their \`Id\` and \`IsFrozen\` set to \`true\`, then uses \`bulkUpdate\` to perform the mass update operation on the Salesforce org. - **Reporting:** It uses \`generateReports\` to create CSV and XLSX files containing details of the users to be frozen. - **Logging:** Provides clear messages about the number of users found and the success of the freezing process. </details> `; static examples = [ `$ sf hardis:org:user:freeze`, `$ sf hardis:org:user:freeze --target-org my-user@myorg.com`, `$ sf hardis:org:user:freeze --includeprofiles 'Standard'`, `$ sf hardis:org:user:freeze --excludeprofiles 'System Administrator,Some Other Profile'`, ]; // public static args = [{name: 'file'}]; static flags = { // flag with a value (-n, --name=VALUE) name: Flags.string({ char: 'n', description: messages.getMessage('nameFilter'), }), includeprofiles: Flags.string({ char: 'p', description: 'List of profiles that you want to freeze, separated by commas', }), excludeprofiles: Flags.string({ char: 'e', description: 'List of profiles that you want to NOT freeze, separated by commas', }), maxuserdisplay: Flags.integer({ char: 'm', default: 100, description: 'Maximum users to display in logs', }), debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), 'target-org': requiredOrgFlagWithDeprecations, }; // Set this to true if your command requires a project workspace; 'requiresProject' is false by default static requiresProject = false; maxUsersDisplay = 100; debugMode = false; /* jscpd:ignore-end */ async run() { const { flags } = await this.parse(OrgFreezeUser); const includeProfileNames = flags.includeprofiles ? flags.includeprofiles.split(',') : []; const excludeProfileNames = flags.excludeprofiles ? flags.excludeprofiles.split(',') : []; this.maxUsersDisplay = flags.maxuserdisplay || 100; this.debugMode = flags.debug || false; const conn = flags['target-org'].getConnection(); // Select profiles that we want users to be frozen let profileIds = []; let profileNames = []; if (includeProfileNames.length === 0 && excludeProfileNames.length === 0) { // Manual user selection const profilesRes = await promptProfiles(conn, { multiselect: true, message: 'Please select profiles that you do you want to freeze users that are assigned to them ?', returnField: 'record', allowSelectMine: false, allowSelectMineErrorMessage: "If you freeze your own profile, you'll be unable to unfreeze it later 😊", allowSelectAll: false, allowSelectAllErrorMessage: 'You can not select all profiles, keep at least one (usually System Administrator) so you can unfreeze later !', }); profileIds = profilesRes.map((profile) => profile.Id); profileNames = profilesRes.map((profile) => { return [profile.Id, profile.Name]; }); } else if (includeProfileNames.length > 0) { // Use includeprofiles argument const profilesConstraintIn = includeProfileNames.map((profileName) => `'${profileName}'`).join(','); const profilesQuery = `SELECT Id,Name FROM Profile WHERE Name IN (${profilesConstraintIn})`; const profilesQueryRes = await soqlQuery(profilesQuery, conn); if (this.debugMode) { uxLog("log", this, c.grey(`Query result:\n${JSON.stringify(profilesQueryRes, null, 2)}`)); } profileIds = profilesQueryRes.records.map((profile) => profile.Id); profileNames = profilesQueryRes.records.map((profile) => { return [profile.Id, profile.Name]; }); } else if (excludeProfileNames.length > 0) { // Use excludeprofiles argument const profilesConstraintIn = excludeProfileNames.map((profileName) => `'${profileName}'`).join(','); const profilesQuery = `SELECT Id,Name FROM Profile WHERE Name NOT IN (${profilesConstraintIn})`; const profilesQueryRes = await soqlQuery(profilesQuery, conn); if (this.debugMode) { uxLog("log", this, c.grey(`Query result:\n${JSON.stringify(profilesQueryRes, null, 2)}`)); } profileIds = profilesQueryRes.records.map((profile) => profile.Id); profileNames = profilesQueryRes.records.map((profile) => { return [profile.Id, profile.Name]; }); } // List profiles that must be frozen const profileIdsStr = profileIds.map((profileId) => `'${profileId}'`).join(','); // Query users that we want to freeze uxLog("action", this, c.cyan(`Querying User records matching ${c.bold(profileIds.length)} profiles...`)); const userQuery = `SELECT Id,Name,Username,ProfileId FROM User WHERE ProfileId IN (${profileIdsStr}) and IsActive=true`; const userQueryRes = await bulkQuery(userQuery, conn); const usersToFreeze = userQueryRes.records; const userIdsStr = usersToFreeze.map((user) => `'${user.Id}'`).join(','); // Check empty result if (usersToFreeze.length === 0) { const outputString = `No matching user records found with defined profile constraints`; uxLog("warning", this, c.yellow(outputString)); return { outputString }; } // Query related UserLogin records uxLog("action", this, c.cyan(`Querying UserLogin records matching ${c.bold(usersToFreeze.length)} users...`)); const userLoginQuery = `SELECT Id,UserId,IsFrozen FROM UserLogin WHERE UserId IN (${userIdsStr}) and IsFrozen=false`; const userLoginQueryRes = await bulkQuery(userLoginQuery, conn); const userLoginsToFreeze = userLoginQueryRes.records; // Display list of users to freeze const usersToFreezeDisplay = userLoginsToFreeze.map((userLogin) => { const matchingUser = usersToFreeze.filter((user) => user.Id === userLogin.UserId)[0]; return { Username: matchingUser.Username, Name: matchingUser.Name, Profile: profileNames.filter((profile) => profile[0] === matchingUser.ProfileId)[1], }; }); uxLog("action", this, c.cyan(`List of ${userLoginsToFreeze.length} users that will be frozen:`)); uxLogTable(this, this.debugMode ? usersToFreezeDisplay : usersToFreezeDisplay.slice(0, this.maxUsersDisplay)); if (!this.debugMode && usersToFreezeDisplay.length > this.maxUsersDisplay) { uxLog("warning", this, c.yellow(c.italic(`(list truncated to the first ${this.maxUsersDisplay} users)`))); } // Generate csv + xls of users about to be frozen await generateReports(usersToFreezeDisplay, ['Username', 'Name', 'Profile'], this, { logFileName: 'users-to-freeze', logLabel: 'Extract of users to freeze', }); // Request configuration from user if (!isCI) { const confirmfreeze = await prompts({ type: 'confirm', name: 'value', initial: true, message: c.cyanBright(`Are you sure you want to freeze these ${c.bold(userLoginsToFreeze.length)} users in org ${c.green(flags['target-org'].getUsername())} ?`), description: 'Confirm freezing selected users, which will deactivate their accounts in the Salesforce org', }); if (confirmfreeze.value !== true) { const outputString = 'Script cancelled by user.'; uxLog("warning", this, c.yellow(outputString)); return { outputString }; } } // Process UserLogin freezing const userLoginsFrozen = userLoginsToFreeze.map((userLogin) => { return { Id: userLogin.Id, IsFrozen: true }; }); const bulkUpdateRes = await bulkUpdate('UserLogin', 'update', userLoginsFrozen, conn); const freezeSuccessNb = bulkUpdateRes.successfulResults.length; const freezeErrorsNb = bulkUpdateRes.failedResults.length; if (freezeErrorsNb > 0) { uxLog("warning", this, c.yellow(`Warning: ${c.red(c.bold(freezeErrorsNb))} users has not been frozen (bulk API errors)`)); } // Build results summary uxLog("success", this, c.green(`${c.bold(freezeSuccessNb)} users has been be frozen.`)); // Return an object to be displayed with --json return { orgId: flags['target-org'].getOrgId(), freezeSuccess: freezeSuccessNb, freezeErrors: freezeErrorsNb, outputString: `${freezeSuccessNb} users has been be frozen`, }; } } //# sourceMappingURL=freeze.js.map