@salesforce/plugin-user
Version:
Commands to interact with Users and Permission Sets
294 lines • 13.9 kB
JavaScript
/*
* 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