@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
448 lines • 19.9 kB
JavaScript
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.User = exports.DefaultUserFields = exports.REQUIRED_FIELDS = void 0;
const node_os_1 = require("node:os");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const http_api_1 = require("@jsforce/jsforce-node/lib/http-api");
const logger_1 = require("../logger/logger");
const messages_1 = require("../messages");
const secureBuffer_1 = require("../crypto/secureBuffer");
const sfError_1 = require("../sfError");
const sfdc_1 = require("../util/sfdc");
const connection_1 = require("./connection");
const permissionSetAssignment_1 = require("./permissionSetAssignment");
const authInfo_1 = require("./authInfo");
const rand = (len) => Math.floor(Math.random() * len.length);
const CHARACTERS = {
LOWER: 'abcdefghijklmnopqrstuvwxyz',
UPPER: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
NUMBERS: '1234567890',
SYMBOLS: ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '[', ']', '|', '-'],
};
const PASSWORD_COMPLEXITY = {
'0': { LOWER: true },
'1': { LOWER: true, NUMBERS: true },
'2': { LOWER: true, SYMBOLS: true },
'3': { LOWER: true, UPPER: true, NUMBERS: true },
'4': { LOWER: true, NUMBERS: true, SYMBOLS: true },
'5': { LOWER: true, UPPER: true, NUMBERS: true, SYMBOLS: true },
};
const scimEndpoint = '/services/scim/v1/Users';
const scimHeaders = { 'auto-approve-user': 'true' };
;
const messages = new messages_1.Messages('@salesforce/core', 'user', new Map([["orgRequired", "An org instance is required."], ["userQueryFailed", "Failed to query for the user %s."], ["invalidHttpResponseCreatingUser", "An invalid http response code (%s) was received while trying to create a user."], ["missingFields", "The fields parameters is undefined."], ["missingId", "The Salesforce id for the user is required."], ["permsetNamesAreRequired", "The permission set names are missing but required."], ["complexityOutOfBound", "Invalid complexity value. Specify a value between 0 and 5, inclusive."], ["lengthOutOfBound", "Invalid length value. Specify a value between 8 and 1000, inclusive."], ["problemsDescribingTheUserObject", "Problems occurred while attempting to describe the user object."]]));
/**
* A Map of Required Salesforce User fields.
*/
exports.REQUIRED_FIELDS = {
id: 'id',
username: 'username',
lastName: 'lastName',
alias: 'alias',
timeZoneSidKey: 'timeZoneSidKey',
localeSidKey: 'localeSidKey',
emailEncodingKey: 'emailEncodingKey',
profileId: 'profileId',
languageLocaleKey: 'languageLocaleKey',
email: 'email',
};
/**
* Helper method to lookup UserFields.
*
* @param logger
* @param username The username.
*/
async function retrieveUserFields(logger, username) {
const connection = await connection_1.Connection.create({
authInfo: await authInfo_1.AuthInfo.create({ username }),
});
const resolvedUsername = await resolveUsernameFromAccessToken(logger)(connection)(username);
const fromFields = Object.keys(exports.REQUIRED_FIELDS).map(kit_1.upperFirst).filter(ts_types_1.isString).join(', ');
const requiredFieldsFromAdminQuery = `SELECT ${fromFields} FROM User WHERE Username='${resolvedUsername}'`;
const result = await connection.query(requiredFieldsFromAdminQuery);
logger.debug('Successfully retrieved the admin user for this org.');
if (result.totalSize === 1) {
const results = (0, kit_1.mapKeys)(result.records[0], (value, key) => (0, kit_1.lowerFirst)(key));
const fields = {
id: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.id]),
username,
alias: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.alias]),
email: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.email]),
emailEncodingKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.emailEncodingKey]),
languageLocaleKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.languageLocaleKey]),
localeSidKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.localeSidKey]),
profileId: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.profileId]),
lastName: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.lastName]),
timeZoneSidKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.timeZoneSidKey]),
};
return fields;
}
else {
throw messages.createError('userQueryFailed', [resolvedUsername]);
}
}
/**
* Gets the profile id associated with a profile name.
*
* @param name The name of the profile.
* @param connection The connection for the query.
*/
async function retrieveProfileId(name, connection) {
if (!(0, sfdc_1.validateSalesforceId)(name)) {
const profileQuery = `SELECT Id FROM Profile WHERE name='${name}'`;
const result = await connection.query(profileQuery);
if (result.records.length > 0) {
return result.records[0].Id;
}
}
return name;
}
/**
* Provides a default set of fields values that can be used to create a user. This is handy for
* software development purposes.
*
* ```
* const connection: Connection = await Connection.create({
* authInfo: await AuthInfo.create({ username: 'user@example.com' })
* });
* const org: Org = await Org.create({ connection });
* const options: DefaultUserFields.Options = {
* templateUser: org.getUsername()
* };
* const fields = (await DefaultUserFields.create(options)).getFields();
* ```
*/
class DefaultUserFields extends kit_1.AsyncCreatable {
// Initialized in init
logger;
userFields;
options;
/**
* @ignore
*/
constructor(options) {
super(options);
this.options = options || { templateUser: '' };
}
/**
* Get user fields.
*/
getFields() {
return this.userFields;
}
/**
* Initialize asynchronous components.
*/
async init() {
this.logger = await logger_1.Logger.child('DefaultUserFields');
this.userFields = await retrieveUserFields(this.logger, this.options.templateUser);
this.userFields.profileId = await retrieveProfileId('Standard User', await connection_1.Connection.create({
authInfo: await authInfo_1.AuthInfo.create({ username: this.options.templateUser }),
}));
this.logger.debug(`Standard User profileId: ${this.userFields.profileId}`);
if (this.options.newUserName) {
this.userFields.username = this.options.newUserName;
}
else {
this.userFields.username = `${Date.now()}_${this.userFields.username}`;
}
}
}
exports.DefaultUserFields = DefaultUserFields;
/**
* A class for creating a User, generating a password for a user, and assigning a user to one or more permission sets.
* See methods for examples.
*/
class User extends kit_1.AsyncCreatable {
org;
logger;
/**
* @ignore
*/
constructor(options) {
super(options);
this.org = options.org;
}
/**
* Generate default password for a user. Returns An encrypted buffer containing a utf8 encoded password.
*/
static generatePasswordUtf8(passwordCondition = { length: 13, complexity: 5 }) {
if (!PASSWORD_COMPLEXITY[passwordCondition.complexity]) {
const msg = messages.getMessage('complexityOutOfBound');
throw new sfError_1.SfError(msg, 'complexityOutOfBound');
}
if (passwordCondition.length < 8 || passwordCondition.length > 1000) {
const msg = messages.getMessage('lengthOutOfBound');
throw new sfError_1.SfError(msg, 'lengthOutOfBound');
}
let password = [];
['SYMBOLS', 'NUMBERS', 'UPPER', 'LOWER'].forEach((charSet) => {
if (PASSWORD_COMPLEXITY[passwordCondition.complexity][charSet]) {
password.push(CHARACTERS[charSet][rand(CHARACTERS[charSet])]);
}
});
// Concatinating remaining length randomly with all lower characters
password = password.concat(Array(Math.max(passwordCondition.length - password.length, 0))
.fill('0')
.map(() => CHARACTERS['LOWER'][rand(CHARACTERS['LOWER'])]));
password = password.sort(() => Math.random() - 0.5);
const secureBuffer = new secureBuffer_1.SecureBuffer();
secureBuffer.consume(Buffer.from(password.join(''), 'utf8'));
return secureBuffer;
}
/**
* Initialize a new instance of a user and return it.
*/
async init() {
this.logger = await logger_1.Logger.child('User');
await this.org.refreshAuth();
this.logger.debug('Auth refresh ok');
}
/**
* Assigns a password to a user. For a user to have the ability to assign their own password, the org needs the
* following org feature: EnableSetPasswordInApi.
*
* @param info The AuthInfo object for user to assign the password to.
* @param password [throwWhenRemoveFails = User.generatePasswordUtf8()] A SecureBuffer containing the new password.
*/
async assignPassword(info, password = User.generatePasswordUtf8()) {
this.logger.debug(`Attempting to set password for userId: ${info.getFields().userId} username: ${info.getFields().username}`);
const userConnection = await connection_1.Connection.create({ authInfo: info });
return new Promise((resolve, reject) => {
// no promises in async method
// eslint-disable-next-line @typescript-eslint/no-misused-promises
password.value(async (buffer) => {
try {
const soap = userConnection.soap;
await soap.setPassword((0, ts_types_1.ensureString)(info.getFields().userId), buffer.toString('utf8'));
this.logger.debug(`Set password for userId: ${info.getFields().userId}`);
resolve();
}
catch (e) {
reject(e);
}
});
});
}
/**
* Methods to assign one or more permission set names to a user.
*
* @param id The Salesforce id of the user to assign the permission set to.
* @param permsetNames An array of permission set names.
*
* ```
* const username = 'user@example.com';
* const connection: Connection = await Connection.create({
* authInfo: await AuthInfo.create({ username })
* });
* const org = await Org.create({ connection });
* const user: User = await User.create({ org });
* const fields: UserFields = await user.retrieve(username);
* await user.assignPermissionSets(fields.id, ['sfdx', 'approver']);
* ```
*/
async assignPermissionSets(id, permsetNames) {
if (!id) {
throw messages.createError('missingId');
}
if (!permsetNames) {
throw messages.createError('permsetNamesAreRequired');
}
const assignments = await permissionSetAssignment_1.PermissionSetAssignment.init(this.org);
await Promise.all(permsetNames.map((permsetName) => assignments.create(id, permsetName)));
}
/**
* Method for creating a new User.
*
* By default scratch orgs only allow creating 2 additional users. Work with Salesforce Customer Service to increase
* user limits.
*
* The Org Preferences required to increase the number of users are:
* Standard User Licenses
* Salesforce CRM Content User
*
* @param fields The required fields for creating a user.
*
* ```
* const connection: Connection = await Connection.create({
* authInfo: await AuthInfo.create({ username: 'user@example.com' })
* });
* const org = await Org.create({ connection });
*
* const defaultUserFields = await DefaultUserFields.create({ templateUser: 'devhub_user@example.com' });
* const user: User = await User.create({ org });
* const info: AuthInfo = await user.createUser(defaultUserFields.getFields());
* ```
*/
async createUser(fields) {
// Create a user and get a refresh token
const refreshTokenSecret = await this.createUserInternal(fields);
// Create the initial auth info
const authInfo = await authInfo_1.AuthInfo.create({ username: this.org.getUsername() });
const adminUserAuthFields = authInfo.getFields(true);
// Setup oauth options for the new user
const oauthOptions = {
// Communities users require the instance for auth
loginUrl: adminUserAuthFields.instanceUrl ?? adminUserAuthFields.loginUrl,
refreshToken: refreshTokenSecret.buffer.value((buffer) => buffer.toString('utf8')),
clientId: adminUserAuthFields.clientId,
clientSecret: adminUserAuthFields.clientSecret,
privateKey: adminUserAuthFields.privateKey,
};
// Create an auth info object for the new user
const newUserAuthInfo = await authInfo_1.AuthInfo.create({
username: fields.username,
oauth2Options: oauthOptions,
});
// Update the auth info object with created user id.
newUserAuthInfo.update({ userId: refreshTokenSecret.userId });
// Make sure we can connect and if so save the auth info.
await this.describeUserAndSave(newUserAuthInfo);
// Let the org know there is a new user. See $HOME/.sfdx/[orgid].json for the mapping.
await this.org.addUsername(newUserAuthInfo);
return newUserAuthInfo;
}
/**
* Method to retrieve the UserFields for a user.
*
* @param username The username of the user.
*
* ```
* const username = 'boris@thecat.com';
* const connection: Connection = await Connection.create({
* authInfo: await AuthInfo.create({ username })
* });
* const org = await Org.create({ connection });
* const user: User = await User.create({ org });
* const fields: UserFields = await user.retrieve(username);
* ```
*/
async retrieve(username) {
return retrieveUserFields(this.logger, username);
}
/**
* Helper method that verifies the server's User object is available and if so allows persisting the Auth information.
*
* @param newUserAuthInfo The AuthInfo for the new user.
*/
async describeUserAndSave(newUserAuthInfo) {
const connection = await connection_1.Connection.create({ authInfo: newUserAuthInfo });
this.logger.debug(`Created connection for user: ${newUserAuthInfo.getUsername()}`);
const userDescribe = await connection.describe('User');
if (userDescribe?.fields) {
await newUserAuthInfo.save();
return newUserAuthInfo;
}
else {
throw messages.createError('permsetNamesAreRequired');
}
}
/**
* Helper that makes a REST request to create the user, and update additional required fields.
*
* @param fields The configuration the new user should have.
*/
async createUserInternal(fields) {
if (!fields) {
throw messages.createError('missingFields');
}
const conn = this.org.getConnection();
const body = JSON.stringify({
username: fields.username,
emails: [fields.email],
name: {
familyName: fields.lastName,
},
nickName: fields.username.substring(0, 40), // nickName has a max length of 40
entitlements: [
{
value: fields.profileId,
},
],
});
this.logger.debug(`user create request body: ${body}`);
const scimUrl = conn.normalizeUrl(scimEndpoint);
this.logger.debug(`scimUrl: ${scimUrl}`);
const info = {
method: 'POST',
url: scimUrl,
headers: scimHeaders,
body,
};
const response = await this.rawRequest(conn, info);
const responseBody = (0, kit_1.parseJsonMap)((0, ts_types_1.ensureString)(response['body']));
const statusCode = (0, ts_types_1.asNumber)(response.statusCode);
this.logger.debug(`user create response.statusCode: ${response.statusCode}`);
if (!(statusCode === 201 || statusCode === 200)) {
let message = messages.getMessage('invalidHttpResponseCreatingUser', [statusCode]);
if (responseBody) {
const errors = (0, ts_types_1.asJsonArray)(responseBody.Errors);
if (errors && errors.length > 0) {
message = `${message} causes:${node_os_1.EOL}`;
errors.forEach((singleMessage) => {
if (!(0, ts_types_1.isJsonMap)(singleMessage))
return;
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message = `${message}${node_os_1.EOL}${singleMessage.description}`;
});
}
}
this.logger.debug(message);
throw new sfError_1.SfError(message, 'UserCreateHttpError');
}
const fieldsWithId = { ...fields, id: (0, ts_types_1.ensureString)(responseBody.id) };
await this.updateRequiredUserFields(fieldsWithId);
const buffer = new secureBuffer_1.SecureBuffer();
const headers = (0, ts_types_1.ensureJsonMap)(response.headers);
const autoApproveUser = (0, ts_types_1.ensureString)(headers['auto-approve-user']);
buffer.consume(Buffer.from(autoApproveUser));
return {
buffer,
userId: fieldsWithId.id,
};
}
// eslint-disable-next-line class-methods-use-this
async rawRequest(conn, options) {
return new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
const httpApi = new http_api_1.HttpApi(conn, options);
httpApi.on('response', (response) => resolve(response));
httpApi.request(options).catch(reject);
});
}
/**
* Update the remaining required fields for the user.
*
* @param fields The fields for the user.
*/
async updateRequiredUserFields(fields) {
const leftOverRequiredFields = (0, kit_1.omit)(fields, [
exports.REQUIRED_FIELDS.username,
exports.REQUIRED_FIELDS.email,
exports.REQUIRED_FIELDS.lastName,
exports.REQUIRED_FIELDS.profileId,
]);
const object = (0, kit_1.mapKeys)(leftOverRequiredFields, (value, key) => (0, kit_1.upperFirst)(key));
await this.org.getConnection().sobject('User').update(object);
this.logger.debug(`Successfully Updated additional properties for user: ${fields.username}`);
}
}
exports.User = User;
const resolveUsernameFromAccessToken = (logger) => (conn) => async (usernameOrAccessToken) => {
if ((0, sfdc_1.matchesAccessToken)(usernameOrAccessToken)) {
logger.debug('received an accessToken for the username. Converting...');
const username = (await conn.identity()).username;
logger.debug(`accessToken converted to ${username}`);
return username;
}
logger.debug('not a accessToken');
return usernameOrAccessToken;
};
//# sourceMappingURL=user.js.map
;