UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

429 lines 18.3 kB
"use strict"; /* * 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