UNPKG

19 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright (c) 2020, salesforce.com, inc.
4 * All rights reserved.
5 * Licensed under the BSD 3-Clause license.
6 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9exports.User = exports.DefaultUserFields = exports.REQUIRED_FIELDS = void 0;
10const os_1 = require("os");
11const kit_1 = require("@salesforce/kit");
12const ts_types_1 = require("@salesforce/ts-types");
13const http_api_1 = require("jsforce/lib/http-api");
14const logger_1 = require("../logger");
15const messages_1 = require("../messages");
16const secureBuffer_1 = require("../crypto/secureBuffer");
17const sfError_1 = require("../sfError");
18const sfdc_1 = require("../util/sfdc");
19const connection_1 = require("./connection");
20const permissionSetAssignment_1 = require("./permissionSetAssignment");
21const authInfo_1 = require("./authInfo");
22const rand = (len) => Math.floor(Math.random() * len.length);
23const CHARACTERS = {
24 LOWER: 'abcdefghijklmnopqrstuvwxyz',
25 UPPER: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
26 NUMBERS: '1234567890',
27 SYMBOLS: ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '[', ']', '|', '-'],
28};
29const PASSWORD_COMPLEXITY = {
30 '0': { LOWER: true },
31 '1': { LOWER: true, NUMBERS: true },
32 '2': { LOWER: true, SYMBOLS: true },
33 '3': { LOWER: true, UPPER: true, NUMBERS: true },
34 '4': { LOWER: true, NUMBERS: true, SYMBOLS: true },
35 '5': { LOWER: true, UPPER: true, NUMBERS: true, SYMBOLS: true },
36};
37const scimEndpoint = '/services/scim/v1/Users';
38const scimHeaders = { 'auto-approve-user': 'true' };
39messages_1.Messages.importMessagesDirectory(__dirname);
40const messages = messages_1.Messages.load('@salesforce/core', 'user', [
41 'invalidHttpResponseCreatingUser',
42 'userQueryFailed',
43 'missingId',
44 'permsetNamesAreRequired',
45 'missingFields',
46 'lengthOutOfBound',
47 'complexityOutOfBound',
48]);
49/**
50 * A Map of Required Salesforce User fields.
51 */
52exports.REQUIRED_FIELDS = {
53 id: 'id',
54 username: 'username',
55 lastName: 'lastName',
56 alias: 'alias',
57 timeZoneSidKey: 'timeZoneSidKey',
58 localeSidKey: 'localeSidKey',
59 emailEncodingKey: 'emailEncodingKey',
60 profileId: 'profileId',
61 languageLocaleKey: 'languageLocaleKey',
62 email: 'email',
63};
64/**
65 * Helper method to lookup UserFields.
66 *
67 * @param logger
68 * @param username The username.
69 */
70async function retrieveUserFields(logger, username) {
71 const connection = await connection_1.Connection.create({
72 authInfo: await authInfo_1.AuthInfo.create({ username }),
73 });
74 if ((0, sfdc_1.matchesAccessToken)(username)) {
75 logger.debug('received an accessToken for the username. Converting...');
76 username = (await connection.identity()).username;
77 logger.debug(`accessToken converted to ${username}`);
78 }
79 else {
80 logger.debug('not a accessToken');
81 }
82 const fromFields = Object.keys(exports.REQUIRED_FIELDS).map(kit_1.upperFirst);
83 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
84 const requiredFieldsFromAdminQuery = `SELECT ${fromFields} FROM User WHERE Username='${username}'`;
85 const result = await connection.query(requiredFieldsFromAdminQuery);
86 logger.debug('Successfully retrieved the admin user for this org.');
87 if (result.totalSize === 1) {
88 const results = (0, kit_1.mapKeys)(result.records[0], (value, key) => (0, kit_1.lowerFirst)(key));
89 const fields = {
90 id: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.id]),
91 username,
92 alias: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.alias]),
93 email: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.email]),
94 emailEncodingKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.emailEncodingKey]),
95 languageLocaleKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.languageLocaleKey]),
96 localeSidKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.localeSidKey]),
97 profileId: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.profileId]),
98 lastName: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.lastName]),
99 timeZoneSidKey: (0, ts_types_1.ensureString)(results[exports.REQUIRED_FIELDS.timeZoneSidKey]),
100 };
101 return fields;
102 }
103 else {
104 throw messages.createError('userQueryFailed', [username]);
105 }
106}
107/**
108 * Gets the profile id associated with a profile name.
109 *
110 * @param name The name of the profile.
111 * @param connection The connection for the query.
112 */
113async function retrieveProfileId(name, connection) {
114 if (!(0, sfdc_1.validateSalesforceId)(name)) {
115 const profileQuery = `SELECT Id FROM Profile WHERE name='${name}'`;
116 const result = await connection.query(profileQuery);
117 if (result.records.length > 0) {
118 return result.records[0].Id;
119 }
120 }
121 return name;
122}
123/**
124 * Provides a default set of fields values that can be used to create a user. This is handy for
125 * software development purposes.
126 *
127 * ```
128 * const connection: Connection = await Connection.create({
129 * authInfo: await AuthInfo.create({ username: 'user@example.com' })
130 * });
131 * const org: Org = await Org.create({ connection });
132 * const options: DefaultUserFields.Options = {
133 * templateUser: org.getUsername()
134 * };
135 * const fields = (await DefaultUserFields.create(options)).getFields();
136 * ```
137 */
138class DefaultUserFields extends kit_1.AsyncCreatable {
139 /**
140 * @ignore
141 */
142 constructor(options) {
143 super(options);
144 this.options = options || { templateUser: '' };
145 }
146 /**
147 * Get user fields.
148 */
149 getFields() {
150 return this.userFields;
151 }
152 /**
153 * Initialize asynchronous components.
154 */
155 async init() {
156 this.logger = await logger_1.Logger.child('DefaultUserFields');
157 this.userFields = await retrieveUserFields(this.logger, this.options.templateUser);
158 this.userFields.profileId = await retrieveProfileId('Standard User', await connection_1.Connection.create({
159 authInfo: await authInfo_1.AuthInfo.create({ username: this.options.templateUser }),
160 }));
161 this.logger.debug(`Standard User profileId: ${this.userFields.profileId}`);
162 if (this.options.newUserName) {
163 this.userFields.username = this.options.newUserName;
164 }
165 else {
166 this.userFields.username = `${Date.now()}_${this.userFields.username}`;
167 }
168 }
169}
170exports.DefaultUserFields = DefaultUserFields;
171/**
172 * A class for creating a User, generating a password for a user, and assigning a user to one or more permission sets.
173 * See methods for examples.
174 */
175class User extends kit_1.AsyncCreatable {
176 /**
177 * @ignore
178 */
179 constructor(options) {
180 super(options);
181 this.org = options.org;
182 }
183 /**
184 * Generate default password for a user. Returns An encrypted buffer containing a utf8 encoded password.
185 */
186 static generatePasswordUtf8(passwordCondition = { length: 13, complexity: 5 }) {
187 if (!PASSWORD_COMPLEXITY[passwordCondition.complexity]) {
188 const msg = messages.getMessage('complexityOutOfBound');
189 throw new sfError_1.SfError(msg, 'complexityOutOfBound');
190 }
191 if (passwordCondition.length < 8 || passwordCondition.length > 1000) {
192 const msg = messages.getMessage('lengthOutOfBound');
193 throw new sfError_1.SfError(msg, 'lengthOutOfBound');
194 }
195 let password = [];
196 ['SYMBOLS', 'NUMBERS', 'UPPER', 'LOWER'].forEach((charSet) => {
197 if (PASSWORD_COMPLEXITY[passwordCondition.complexity][charSet]) {
198 password.push(CHARACTERS[charSet][rand(CHARACTERS[charSet])]);
199 }
200 });
201 // Concatinating remaining length randomly with all lower characters
202 password = password.concat(Array(Math.max(passwordCondition.length - password.length, 0))
203 .fill('0')
204 .map(() => CHARACTERS['LOWER'][rand(CHARACTERS['LOWER'])]));
205 password = password.sort(() => Math.random() - 0.5);
206 const secureBuffer = new secureBuffer_1.SecureBuffer();
207 secureBuffer.consume(Buffer.from(password.join(''), 'utf8'));
208 return secureBuffer;
209 }
210 /**
211 * Initialize a new instance of a user and return it.
212 */
213 async init() {
214 this.logger = await logger_1.Logger.child('User');
215 await this.org.refreshAuth();
216 this.logger.debug('Auth refresh ok');
217 }
218 /**
219 * Assigns a password to a user. For a user to have the ability to assign their own password, the org needs the
220 * following org feature: EnableSetPasswordInApi.
221 *
222 * @param info The AuthInfo object for user to assign the password to.
223 * @param password [throwWhenRemoveFails = User.generatePasswordUtf8()] A SecureBuffer containing the new password.
224 */
225 async assignPassword(info, password = User.generatePasswordUtf8()) {
226 this.logger.debug(`Attempting to set password for userId: ${info.getFields().userId} username: ${info.getFields().username}`);
227 const userConnection = await connection_1.Connection.create({ authInfo: info });
228 return new Promise((resolve, reject) => {
229 // no promises in async method
230 // eslint-disable-next-line @typescript-eslint/no-misused-promises
231 password.value(async (buffer) => {
232 try {
233 const soap = userConnection.soap;
234 await soap.setPassword((0, ts_types_1.ensureString)(info.getFields().userId), buffer.toString('utf8'));
235 this.logger.debug(`Set password for userId: ${info.getFields().userId}`);
236 resolve();
237 }
238 catch (e) {
239 reject(e);
240 }
241 });
242 });
243 }
244 /**
245 * Methods to assign one or more permission set names to a user.
246 *
247 * @param id The Salesforce id of the user to assign the permission set to.
248 * @param permsetNames An array of permission set names.
249 *
250 * ```
251 * const username = 'user@example.com';
252 * const connection: Connection = await Connection.create({
253 * authInfo: await AuthInfo.create({ username })
254 * });
255 * const org = await Org.create({ connection });
256 * const user: User = await User.create({ org });
257 * const fields: UserFields = await user.retrieve(username);
258 * await user.assignPermissionSets(fields.id, ['sfdx', 'approver']);
259 * ```
260 */
261 async assignPermissionSets(id, permsetNames) {
262 if (!id) {
263 throw messages.createError('missingId');
264 }
265 if (!permsetNames) {
266 throw messages.createError('permsetNamesAreRequired');
267 }
268 const assignments = await permissionSetAssignment_1.PermissionSetAssignment.init(this.org);
269 await Promise.all(permsetNames.map((permsetName) => assignments.create(id, permsetName)));
270 }
271 /**
272 * Method for creating a new User.
273 *
274 * By default scratch orgs only allow creating 2 additional users. Work with Salesforce Customer Service to increase
275 * user limits.
276 *
277 * The Org Preferences required to increase the number of users are:
278 * Standard User Licenses
279 * Salesforce CRM Content User
280 *
281 * @param fields The required fields for creating a user.
282 *
283 * ```
284 * const connection: Connection = await Connection.create({
285 * authInfo: await AuthInfo.create({ username: 'user@example.com' })
286 * });
287 * const org = await Org.create({ connection });
288 *
289 * const defaultUserFields = await DefaultUserFields.create({ templateUser: 'devhub_user@example.com' });
290 * const user: User = await User.create({ org });
291 * const info: AuthInfo = await user.createUser(defaultUserFields.getFields());
292 * ```
293 */
294 async createUser(fields) {
295 // Create a user and get a refresh token
296 const refreshTokenSecret = await this.createUserInternal(fields);
297 // Create the initial auth info
298 const authInfo = await authInfo_1.AuthInfo.create({ username: this.org.getUsername() });
299 const adminUserAuthFields = authInfo.getFields(true);
300 // Setup oauth options for the new user
301 const oauthOptions = {
302 // Communities users require the instance for auth
303 loginUrl: adminUserAuthFields.instanceUrl ?? adminUserAuthFields.loginUrl,
304 refreshToken: refreshTokenSecret.buffer.value((buffer) => buffer.toString('utf8')),
305 clientId: adminUserAuthFields.clientId,
306 clientSecret: adminUserAuthFields.clientSecret,
307 privateKey: adminUserAuthFields.privateKey,
308 };
309 // Create an auth info object for the new user
310 const newUserAuthInfo = await authInfo_1.AuthInfo.create({
311 username: fields.username,
312 oauth2Options: oauthOptions,
313 });
314 // Update the auth info object with created user id.
315 const newUserAuthFields = newUserAuthInfo.getFields();
316 newUserAuthFields.userId = refreshTokenSecret.userId;
317 // Make sure we can connect and if so save the auth info.
318 await this.describeUserAndSave(newUserAuthInfo);
319 // Let the org know there is a new user. See $HOME/.sfdx/[orgid].json for the mapping.
320 await this.org.addUsername(newUserAuthInfo);
321 return newUserAuthInfo;
322 }
323 /**
324 * Method to retrieve the UserFields for a user.
325 *
326 * @param username The username of the user.
327 *
328 * ```
329 * const username = 'boris@thecat.com';
330 * const connection: Connection = await Connection.create({
331 * authInfo: await AuthInfo.create({ username })
332 * });
333 * const org = await Org.create({ connection });
334 * const user: User = await User.create({ org });
335 * const fields: UserFields = await user.retrieve(username);
336 * ```
337 */
338 async retrieve(username) {
339 return retrieveUserFields(this.logger, username);
340 }
341 /**
342 * Helper method that verifies the server's User object is available and if so allows persisting the Auth information.
343 *
344 * @param newUserAuthInfo The AuthInfo for the new user.
345 */
346 async describeUserAndSave(newUserAuthInfo) {
347 const connection = await connection_1.Connection.create({ authInfo: newUserAuthInfo });
348 this.logger.debug(`Created connection for user: ${newUserAuthInfo.getUsername()}`);
349 const userDescribe = await connection.describe('User');
350 if (userDescribe?.fields) {
351 await newUserAuthInfo.save();
352 return newUserAuthInfo;
353 }
354 else {
355 throw messages.createError('permsetNamesAreRequired');
356 }
357 }
358 /**
359 * Helper that makes a REST request to create the user, and update additional required fields.
360 *
361 * @param fields The configuration the new user should have.
362 */
363 async createUserInternal(fields) {
364 if (!fields) {
365 throw messages.createError('missingFields');
366 }
367 const conn = this.org.getConnection();
368 const body = JSON.stringify({
369 username: fields.username,
370 emails: [fields.email],
371 name: {
372 familyName: fields.lastName,
373 },
374 nickName: fields.username.substring(0, 40),
375 entitlements: [
376 {
377 value: fields.profileId,
378 },
379 ],
380 });
381 this.logger.debug(`user create request body: ${body}`);
382 const scimUrl = conn.normalizeUrl(scimEndpoint);
383 this.logger.debug(`scimUrl: ${scimUrl}`);
384 const info = {
385 method: 'POST',
386 url: scimUrl,
387 headers: scimHeaders,
388 body,
389 };
390 const response = await this.rawRequest(conn, info);
391 const responseBody = (0, kit_1.parseJsonMap)((0, ts_types_1.ensureString)(response['body']));
392 const statusCode = (0, ts_types_1.asNumber)(response.statusCode);
393 this.logger.debug(`user create response.statusCode: ${response.statusCode}`);
394 if (!(statusCode === 201 || statusCode === 200)) {
395 let message = messages.getMessage('invalidHttpResponseCreatingUser', [statusCode]);
396 if (responseBody) {
397 const errors = (0, ts_types_1.asJsonArray)(responseBody.Errors);
398 if (errors && errors.length > 0) {
399 message = `${message} causes:${os_1.EOL}`;
400 errors.forEach((singleMessage) => {
401 if (!(0, ts_types_1.isJsonMap)(singleMessage))
402 return;
403 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
404 message = `${message}${os_1.EOL}${singleMessage.description}`;
405 });
406 }
407 }
408 this.logger.debug(message);
409 throw new sfError_1.SfError(message, 'UserCreateHttpError');
410 }
411 fields.id = (0, ts_types_1.ensureString)(responseBody.id);
412 await this.updateRequiredUserFields(fields);
413 const buffer = new secureBuffer_1.SecureBuffer();
414 const headers = (0, ts_types_1.ensureJsonMap)(response.headers);
415 const autoApproveUser = (0, ts_types_1.ensureString)(headers['auto-approve-user']);
416 buffer.consume(Buffer.from(autoApproveUser));
417 return {
418 buffer,
419 userId: fields.id,
420 };
421 }
422 // eslint-disable-next-line class-methods-use-this
423 async rawRequest(conn, options) {
424 return new Promise((resolve, reject) => {
425 // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
426 const httpApi = new http_api_1.HttpApi(conn, options);
427 httpApi.on('response', (response) => resolve(response));
428 httpApi.request(options).catch(reject);
429 });
430 }
431 /**
432 * Update the remaining required fields for the user.
433 *
434 * @param fields The fields for the user.
435 */
436 async updateRequiredUserFields(fields) {
437 const leftOverRequiredFields = (0, kit_1.omit)(fields, [
438 exports.REQUIRED_FIELDS.username,
439 exports.REQUIRED_FIELDS.email,
440 exports.REQUIRED_FIELDS.lastName,
441 exports.REQUIRED_FIELDS.profileId,
442 ]);
443 const object = (0, kit_1.mapKeys)(leftOverRequiredFields, (value, key) => (0, kit_1.upperFirst)(key));
444 await this.org.getConnection().sobject('User').update(object);
445 this.logger.debug(`Successfully Updated additional properties for user: ${fields.username}`);
446 }
447}
448exports.User = User;
449//# sourceMappingURL=user.js.map
\No newline at end of file