UNPKG

@salesforce/plugin-user

Version:

Commands to interact with Users and Permission Sets

294 lines 13.9 kB
/* * Copyright 2026, Salesforce, Inc. * * 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 { EOL } from 'node:os'; import fs from 'node:fs'; import { DefaultUserFields, Logger, Messages, Org, SfError, StateAggregator, User, } from '@salesforce/core'; import { mapKeys, omit, toBoolean } from '@salesforce/kit'; import { ensureString, getString } from '@salesforce/ts-types'; import { Flags, loglevel, orgApiVersionFlagWithDeprecations, parseVarArgs, requiredOrgFlagWithDeprecations, SfCommand, } from '@salesforce/sf-plugins-core'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-user', 'create'); const secretsMessages = Messages.loadMessages('@salesforce/plugin-user', 'secrets-redacted'); export class CreateUserCommand extends SfCommand { static strict = false; static deprecateAliases = true; static aliases = ['force:user:create']; static summary = messages.getMessage('summary'); static description = messages.getMessage('description'); static examples = messages.getMessages('examples'); static flags = { 'set-alias': Flags.string({ char: 'a', summary: messages.getMessage('flags.set-alias.summary'), aliases: ['setalias'], deprecateAliases: true, }), 'definition-file': Flags.string({ char: 'f', summary: messages.getMessage('flags.definition-file.summary'), description: messages.getMessage('flags.definition-file.description'), aliases: ['definitionfile'], deprecateAliases: true, }), 'set-unique-username': Flags.boolean({ char: 's', summary: messages.getMessage('flags.set-unique-username.summary'), description: messages.getMessage('flags.set-unique-username.description'), aliases: ['setuniqueusername'], deprecateAliases: true, }), 'target-org': requiredOrgFlagWithDeprecations, 'api-version': orgApiVersionFlagWithDeprecations, loglevel, }; successes = []; failures = []; // assert! because they're set early in run method flags; varargs; async run() { const { flags, argv } = await this.parse(CreateUserCommand); this.flags = flags; const logger = await Logger.child(this.constructor.name); this.varargs = parseVarArgs({}, argv); const conn = await getValidatedConnection(flags['target-org'], flags['api-version']); const defaultUserFields = await DefaultUserFields.create({ templateUser: ensureString(flags['target-org'].getUsername()), }); const targetOrgUser = await User.create({ org: flags['target-org'] }); // merge defaults with provided values with cli > file > defaults const fields = await this.aggregateFields(defaultUserFields.getFields(), logger); const newUserAuthInfo = await getNewUserAuthInfo(targetOrgUser, fields, conn); if (fields.profileName) await newUserAuthInfo?.save({ userProfileName: fields.profileName }); // Assign permission sets to the created user if (fields.permsets) { try { // permsets can be passed from cli args or file we need to create an array of permset names either way it's passed // it will either be a comma separated string, or an array, force it into an array const permsetArray = permsetsStringToArray(fields.permsets); await targetOrgUser.assignPermissionSets(ensureString(newUserAuthInfo.getFields().userId), permsetArray); this.successes.push({ name: 'Permission Set Assignment', value: permsetArray.join(','), }); } catch (error) { const err = error; this.failures.push({ name: 'Permission Set Assignment', message: err.message, }); } } // Generate and set a password if specified if (fields.generatePassword) { try { const password = User.generatePasswordUtf8(); await targetOrgUser.assignPassword(newUserAuthInfo, password); password.value((pass) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises newUserAuthInfo.save({ password: pass.toString('utf-8') }); this.successes.push({ name: 'Password Assignment', value: pass.toString(), }); }); this.warn(secretsMessages.getMessage('warning.passwordGenerated')); } catch (error) { const err = error; this.failures.push({ name: 'Password Assignment', message: err.message, }); } } // Set the alias if specified if (flags['set-alias']) { const stateAggregator = await StateAggregator.getInstance(); await stateAggregator.aliases.setAndSave(flags['set-alias'], fields.username); } fields.id = ensureString(newUserAuthInfo.getFields().userId); this.print(fields); this.setExitCode(); const { permsets, ...fieldsWithoutPermsets } = fields; return { orgId: flags['target-org'].getOrgId(), permissionSetAssignments: permsetsStringToArray(permsets), fields: { ...mapKeys(fieldsWithoutPermsets, (value, key) => key.toLowerCase()) }, }; } // this method is pretty rough, so we'll ignore some TS errors async aggregateFields(defaultFields, logger) { // username can be overridden both in the file or varargs, save it to check if it was changed somewhere const defaultUsername = defaultFields.username; // start with the default fields, then add the fields from the file, then (possibly overwriting) add the fields from the cli varargs param if (this.flags['definition-file']) { const content = JSON.parse(await fs.promises.readFile(this.flags['definition-file'], 'utf-8')); Object.entries(content).forEach(([key, value]) => { // @ts-expect-error cast entries to lowercase to standardize defaultFields[lowerFirstLetter(key)] = value; }); } if (this.varargs) { Object.keys(this.varargs).forEach((key) => { if (key.toLowerCase() === 'generatepassword') { // @ts-expect-error standardize generatePassword casing defaultFields['generatePassword'] = toBoolean(this.varargs[key]); } else if (key.toLowerCase() === 'profilename') { // @ts-expect-error standardize profileName casing defaultFields['profileName'] = this.varargs[key]; } else if (key.toLowerCase() === 'roledevelopername') { // @ts-expect-error standardize roleDeveloperName casing defaultFields['roleDeveloperName'] = this.varargs[key]; } else { // @ts-expect-error all other varargs are left "as is" defaultFields[lowerFirstLetter(key)] = this.varargs[key]; } }); } // check if "username" was passed along with "set-unique-username" flag, if so append org id if (this.flags['set-unique-username'] && defaultFields.username !== defaultUsername) { defaultFields.username = `${defaultFields.username}.${this.flags['target-org'].getOrgId().toLowerCase()}`; } // @ts-expect-error check if "profileName" was passed, this needs to become a profileId before calling User.create if (defaultFields['profileName']) { // @ts-expect-error profileName is not a valid field on UserFields const name = (defaultFields['profileName'] ?? 'Standard User'); logger.debug(`Querying org for profile name [${name}]`); const profile = await this.flags['target-org'] .getConnection(this.flags['api-version']) .singleRecordQuery(`SELECT id FROM profile WHERE name='${name}'`); defaultFields.profileId = profile.Id; } // @ts-expect-error check if "roleDeveloperName" was passed, this needs to become a userRoleId before calling User.create if (defaultFields['roleDeveloperName']) { // @ts-expect-error roleDeveloperName is not a valid field on UserFields const devName = defaultFields['roleDeveloperName']; if (!/^[a-z](?!.*__)(?!.*_$)\w*$/i.test(devName)) { throw messages.createError('error.invalidRoleDeveloperName', [devName]); } logger.debug(`Querying org for user role name [${devName}]`); const userRole = await this.flags['target-org'] .getConnection(this.flags['api-version']) .singleRecordQuery(`SELECT id FROM userrole WHERE developername='${devName}'`); // @ts-expect-error userRoleId is an optional field therefore not defined on UserFields defaultFields['userRoleId'] = userRole.Id; } return defaultFields; } print(fields) { const userCreatedSuccessMsg = messages.getMessage('success', [ fields.username, fields.id, this.flags['target-org'].getOrgId(), EOL, this.config.bin, fields.username, ]); // we initialize to be an empty array to be able to push onto it // so we need to check that the size is greater than 0 to know we had a failure if (this.failures.length > 0) { this.styledHeader('Partial Success'); this.log(userCreatedSuccessMsg); this.log(''); this.styledHeader('Failures'); this.table({ data: this.failures, columns: [ { key: 'name', name: 'Action' }, { key: 'message', name: 'Error Message' }, ], }); } else { this.log(userCreatedSuccessMsg); } } setExitCode() { if (this.failures.length && this.successes.length) { process.exitCode = 68; } else if (this.failures.length) { process.exitCode = 1; } else if (this.successes.length) { process.exitCode = 0; } } } export default CreateUserCommand; const lowerFirstLetter = (word) => word[0].toLowerCase() + word.substr(1); /** * removes fields that cause errors in salesforce APIs within sfdx-core's createUser method * * @param fields a list of combined fields from varargs and the config file * @private */ const stripInvalidAPIFields = (fields) => omit(fields, ['permsets', 'generatepassword', 'generatePassword', 'profileName', 'roleDeveloperName']); const getNewUserAuthInfo = async (targetOrgUser, fields, conn) => { try { return await targetOrgUser.createUser(stripInvalidAPIFields(fields)); } catch (e) { if (!(e instanceof Error)) { throw e; } return catchCreateUser(e, fields, conn); } }; /** will definitely throw, but will throw better error messages than User.createUser was going to */ const catchCreateUser = async (respBody, fields, conn) => { // For Gacks, the error message is on response.body[0].message but for handled errors // the error message is on response.body.Errors[0].description. const errMessage = getString(respBody, 'message') ?? 'Unknown Error'; // Provide a more user friendly error message for certain server errors. if (errMessage.includes('LICENSE_LIMIT_EXCEEDED')) { const profile = await conn.singleRecordQuery(`SELECT name FROM profile WHERE id='${fields.profileId}'`); throw new SfError(messages.getMessage('licenseLimitExceeded', [profile.Name]), 'licenseLimitExceeded'); } else if (errMessage.includes('DUPLICATE_USERNAME')) { throw new SfError(messages.getMessage('duplicateUsername', [fields.username]), 'duplicateUsername'); } else { throw SfError.wrap(errMessage); } }; /** the org must be a scratch org AND not use JWT with hyperforce */ const getValidatedConnection = async (targetOrg, apiVersion) => { if (!(await targetOrg.determineIfScratch())) { throw messages.createError('error.nonScratchOrg'); } const conn = targetOrg.getConnection(apiVersion); if (conn.getAuthInfo().isJwt() && // hyperforce sandbox instances end in S like USA254S targetOrg.getField(Org.Fields.CREATED_ORG_INSTANCE)?.endsWith('S')) { throw messages.createError('error.jwtHyperforce'); } return conn; }; const permsetsStringToArray = (fieldsPermsets) => { if (!fieldsPermsets) return []; return Array.isArray(fieldsPermsets) ? fieldsPermsets : fieldsPermsets.split(',').map((item) => item.replace("'", '').trim()); }; //# sourceMappingURL=user.js.map