UNPKG

@salesforce/core

Version:

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

448 lines 19.9 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 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