@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
429 lines • 18.3 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 os_1 = require("os");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const authInfo_1 = require("./authInfo");
const connection_1 = require("./connection");
const logger_1 = require("./logger");
const messages_1 = require("./messages");
const permissionSetAssignment_1 = require("./permissionSetAssignment");
const secureBuffer_1 = require("./secureBuffer");
const sfdxError_1 = require("./sfdxError");
const sfdc_1 = require("./util/sfdc");
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' };
/**
* 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 }),
});
if (sfdc_1.sfdc.matchesAccessToken(username)) {
logger.debug('received an accessToken for the username. Converting...');
username = (await connection.identity()).username;
logger.debug(`accessToken converted to ${username}`);
}
else {
logger.debug('not a accessToken');
}
const fromFields = Object.keys(exports.REQUIRED_FIELDS).map(kit_1.upperFirst);
const requiredFieldsFromAdminQuery = `SELECT ${fromFields} FROM User WHERE Username='${username}'`;
const result = await connection.query(requiredFieldsFromAdminQuery);
logger.debug('Successfully retrieved the admin user for this org.');
if (result.totalSize === 1) {
const results = kit_1.mapKeys(result.records[0], (value, key) => kit_1.lowerFirst(key));
const fields = {
id: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.id)),
username,
alias: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.alias)),
email: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.email)),
emailEncodingKey: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.emailEncodingKey)),
languageLocaleKey: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.languageLocaleKey)),
localeSidKey: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.localeSidKey)),
profileId: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.profileId)),
lastName: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.lastName)),
timeZoneSidKey: ts_types_1.ensure(ts_types_1.getString(results, exports.REQUIRED_FIELDS.timeZoneSidKey)),
};
return fields;
}
else {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'user', 'userQueryFailed', [username]);
}
}
/**
* 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 (!sfdc_1.sfdc.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 {
/**
* @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 {
/**
* @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]) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'user', 'complexityOutOfBound');
}
if (passwordCondition.length < 8 || passwordCondition.length > 1000) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'user', '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) => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
password.value(async (buffer) => {
try {
// @ts-ignore TODO: expose `soap` on Connection however appropriate
const soap = userConnection.soap;
await soap.setPassword(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 sfdxError_1.SfdxError.create('@salesforce/core', 'user', 'missingId');
}
if (!permsetNames) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'user', 'permsetNamesAreRequired');
}
const assignments = await permissionSetAssignment_1.PermissionSetAssignment.init(this.org);
for (const permsetName of permsetNames) {
await 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) {
var _a;
// 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: (_a = adminUserAuthFields.instanceUrl) !== null && _a !== void 0 ? _a : 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.
const newUserAuthFields = newUserAuthInfo.getFields();
newUserAuthFields.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 await 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 && userDescribe.fields) {
await newUserAuthInfo.save();
return newUserAuthInfo;
}
else {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'user', 'problemsDescribingTheUserObject');
}
}
/**
* 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 sfdxError_1.SfdxError.create('@salesforce/core', 'user', '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),
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 conn.requestRaw(info);
const responseBody = kit_1.parseJsonMap(ts_types_1.ensureString(response['body']));
const statusCode = ts_types_1.asNumber(response.statusCode);
this.logger.debug(`user create response.statusCode: ${response.statusCode}`);
if (!(statusCode === 201 || statusCode === 200)) {
const messages = messages_1.Messages.loadMessages('@salesforce/core', 'user');
let message = messages.getMessage('invalidHttpResponseCreatingUser', [statusCode]);
if (responseBody) {
const errors = ts_types_1.asJsonArray(responseBody.Errors);
if (errors && errors.length > 0) {
message = `${message} causes:${os_1.EOL}`;
errors.forEach((singleMessage) => {
if (!ts_types_1.isJsonMap(singleMessage))
return;
message = `${message}${os_1.EOL}${singleMessage.description}`;
});
}
}
this.logger.debug(message);
throw new sfdxError_1.SfdxError(message, 'UserCreateHttpError');
}
fields.id = ts_types_1.ensureString(responseBody.id);
await this.updateRequiredUserFields(fields);
const buffer = new secureBuffer_1.SecureBuffer();
const headers = ts_types_1.ensureJsonMap(response.headers);
const autoApproveUser = ts_types_1.ensureString(headers['auto-approve-user']);
buffer.consume(Buffer.from(autoApproveUser));
return {
buffer,
userId: fields.id,
};
}
/**
* Update the remaining required fields for the user.
*
* @param fields The fields for the user.
*/
async updateRequiredUserFields(fields) {
const leftOverRequiredFields = kit_1.omit(fields, [
exports.REQUIRED_FIELDS.username,
exports.REQUIRED_FIELDS.email,
exports.REQUIRED_FIELDS.lastName,
exports.REQUIRED_FIELDS.profileId,
]);
const object = kit_1.mapKeys(leftOverRequiredFields, (value, key) => 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;
//# sourceMappingURL=user.js.map