Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion messages/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Display information about a Salesforce user.

# description

Output includes the profile name, org ID, access token, instance URL, login URL, and alias if applicable. The displayed alias is local and different from the Alias field of the User sObject record of the new user, which you set in the Setup UI.
Output includes the profile name, org ID, instance URL, login URL, and alias if applicable. The displayed alias is local and different from the Alias field of the User sObject record of the new user, which you set in the Setup UI.

# examples

Expand Down
4 changes: 2 additions & 2 deletions messages/password.generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ To change the password strength, set the --complexity flag to a value between 0
4 - lower and upper case letters and symbols only
5 - lower and upper case letters and numbers and symbols only

To see a password that was previously generated, run "org display user".
To see a password that was previously generated, run "org auth show-user-password".

# examples

Expand Down Expand Up @@ -88,7 +88,7 @@ Successfully set passwords:%s

# viewWithCommand

You can see the password again by running "%s org display user -o %s".
You can see the password again by running "%s org auth show-user-password -o %s".

# flags.target-org.summary

Expand Down
19 changes: 19 additions & 0 deletions messages/secrets-redacted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# redacted.accessToken

[REDACTED] Use 'sf org auth show-access-token' to view

# redacted.userPassword

[REDACTED] Use 'sf org auth show-user-password' to view

# temp.envVarWorkaround

Secrets are now hidden from '%s' command output. Use the 'sf org auth' commands instead. As a temporary workaround, you can set SF_TEMP_SHOW_SECRETS=true to render these secrets. This workaround will be removed in an upcoming release.

# temp.envVarIsSet

The SF_TEMP_SHOW_SECRETS env var is set. This is a temporary env var to continue to show secrets in the '%s' command output. This workaround will be removed in an upcoming CLI release. Switch to use the 'sf org auth' commands to avoid future disruption.

# warning.passwordGenerated

This command has generated and displayed a password. Avoid sharing or logging this output as it contains a sensitive credential.
2 changes: 2 additions & 0 deletions src/baseCommands/user/password/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AuthInfo, Connection, Messages, Org, SfError, StateAggregator, User } f

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'password.generate');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-user', 'secrets-redacted');

export type PasswordData = {
username?: string;
Expand Down Expand Up @@ -90,6 +91,7 @@ export abstract class UserPasswordGenerateBaseCommand extends SfCommand<Generate
}
/* eslint-enable no-await-in-loop */

this.warn(secretsMessages.getMessage('warning.passwordGenerated'));
this.print(passwordData);

return passwordData.length === 1 ? passwordData[0] : passwordData;
Expand Down
6 changes: 4 additions & 2 deletions src/commands/org/create/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { Interfaces } from '@oclif/core';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'create');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-user', 'secrets-redacted');

type SuccessMsg = {
name: string;
Expand Down Expand Up @@ -148,6 +149,7 @@ export class CreateUserCommand extends SfCommand<CreateUserOutput> {
value: pass.toString(),
});
});
this.warn(secretsMessages.getMessage('warning.passwordGenerated'));
} catch (error) {
const err = error as SfError;
this.failures.push({
Expand Down Expand Up @@ -228,8 +230,8 @@ export class CreateUserCommand extends SfCommand<CreateUserOutput> {
// @ts-expect-error roleDeveloperName is not a valid field on UserFields
const devName = defaultFields['roleDeveloperName'] as string;
if (!/^[a-z](?!.*__)(?!.*_$)\w*$/i.test(devName)) {
throw messages.createError('error.invalidRoleDeveloperName', [devName]);
}
throw messages.createError('error.invalidRoleDeveloperName', [devName]);
}
logger.debug(`Querying org for user role name [${devName}]`);
const userRole = await this.flags['target-org']
.getConnection(this.flags['api-version'])
Expand Down
20 changes: 16 additions & 4 deletions src/commands/org/display/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { AuthFields, Connection, Logger, Messages, StateAggregator } from '@salesforce/core';
import { AuthFields, Connection, envVars, Logger, Messages, StateAggregator } from '@salesforce/core';
import { ensureString } from '@salesforce/ts-types';
import {
loglevel,
Expand All @@ -25,6 +25,7 @@ import {

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'display');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-user', 'secrets-redacted');

export type DisplayUserResult = {
username: string;
Expand Down Expand Up @@ -95,8 +96,13 @@ export class DisplayUserCommand extends SfCommand<DisplayUserResult> {
);
}

// TODO: Remove env var workaround
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
const accessTokenRedacted = secretsMessages.getMessage('redacted.accessToken');
const passwordRedacted = secretsMessages.getMessage('redacted.userPassword');

const result: DisplayUserResult = {
accessToken: conn.accessToken as string,
accessToken: showSecretsEnvVarIsSet ? (conn.accessToken as string) : accessTokenRedacted,
id: userId,
instanceUrl: userAuthData?.instanceUrl,
loginUrl: userAuthData?.loginUrl,
Expand All @@ -112,10 +118,16 @@ export class DisplayUserCommand extends SfCommand<DisplayUserResult> {
}

if (userAuthData?.password) {
result.password = userAuthData.password;
result.password = showSecretsEnvVarIsSet ? userAuthData.password : passwordRedacted;
}

if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org display user']));
this.warn(messages.getMessage('securityWarning'));
} else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org display user']));
}

this.warn(messages.getMessage('securityWarning'));
this.log('');
this.print(result);

Expand Down
19 changes: 17 additions & 2 deletions src/commands/org/list/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Connection, Messages, StateAggregator } from '@salesforce/core';
import { Connection, envVars, Messages, StateAggregator } from '@salesforce/core';
import {
loglevel,
orgApiVersionFlagWithDeprecations,
Expand All @@ -24,6 +24,7 @@ import {

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-user', 'list');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-user', 'secrets-redacted');

export type AuthList = Partial<{
defaultMarker: string;
Expand Down Expand Up @@ -68,6 +69,10 @@ export class ListUsersCommand extends SfCommand<ListUsers> {
(await StateAggregator.getInstance()).aliases,
]);

// TODO: Remove env var workaround
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
const accessTokenRedacted = secretsMessages.getMessage('redacted.accessToken');

const authList: ListUsers = userAuthData.map((authData) => {
const username = authData.getUsername();
// if they passed in an alias see if it maps to an Alias.
Expand All @@ -80,7 +85,7 @@ export class ListUsersCommand extends SfCommand<ListUsers> {
username,
profileName,
orgId: flags['target-org']?.getOrgId(),
accessToken: authData.getFields().accessToken,
accessToken: showSecretsEnvVarIsSet ? authData.getFields().accessToken : accessTokenRedacted,
instanceUrl: authData.getFields().instanceUrl,
loginUrl: authData.getFields().loginUrl,
userId: userInfos.get(username)?.Id,
Expand All @@ -96,6 +101,16 @@ export class ListUsersCommand extends SfCommand<ListUsers> {
})),
title: `Users in org ${flags['target-org']?.getOrgId()}`,
});

// TODO: Remove after env var workaround is removed
if (this.jsonEnabled()) {
if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org list users']));
} else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org list users']));
}
}

return authList;
}
}
Expand Down
2 changes: 1 addition & 1 deletion test/allCommands.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('verifies all commands run successfully ', () => {

expect(output?.result.orgId).to.have.length(18);
expect(output?.result.id).to.have.length(18);
expect(output?.result?.accessToken?.startsWith(output?.result.orgId.substr(0, 15))).to.be.true;
expect(output?.result?.accessToken).to.include('[REDACTED]');
mainUserId = output?.result.id;
});

Expand Down
61 changes: 36 additions & 25 deletions test/commands/display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,36 +78,47 @@ describe('org:display:user', () => {

it('should display the correct information from the default user', async () => {
await prepareStubs();
// testUser1@test.com is aliased to testUser
const expected = {
accessToken: defaultOrg.accessToken,
alias: 'testAlias',
id: '1234567890',
instanceUrl,
loginUrl,
orgId: 'abc123',
password: '-a098u234/1!@#',
profileName: 'profileName',
username: 'defaultusername@test.com',
};
const result = await DisplayUserCommand.run(['--json', '--target-org', defaultOrg.username]);
expect(result).to.deep.equal(expected);
expect(result.accessToken).to.include('[REDACTED]');
expect(result.password).to.include('[REDACTED]');
expect(result.alias).to.equal('testAlias');
expect(result.id).to.equal('1234567890');
expect(result.instanceUrl).to.equal(instanceUrl);
expect(result.loginUrl).to.equal(loginUrl);
expect(result.orgId).to.equal('abc123');
expect(result.profileName).to.equal('profileName');
expect(result.username).to.equal('defaultusername@test.com');
});

it('should make queries to the server to get userId and profileName', async () => {
await prepareStubs(true);
// testUser1@test.com is aliased to testUser
const expected = {
accessToken: defaultOrg.accessToken,
alias: 'testAlias',
id: 'QueriedId',
instanceUrl,
loginUrl,
orgId: 'abc123',
profileName: 'QueriedName',
username: defaultOrg.username,
};
const result = await DisplayUserCommand.run(['--json', '--targetusername', 'defaultusername@test.com']);
expect(result).to.deep.equal(expected);
expect(result.accessToken).to.include('[REDACTED]');
expect(result.alias).to.equal('testAlias');
expect(result.id).to.equal('QueriedId');
expect(result.instanceUrl).to.equal(instanceUrl);
expect(result.loginUrl).to.equal(loginUrl);
expect(result.orgId).to.equal('abc123');
expect(result.profileName).to.equal('QueriedName');
expect(result.username).to.equal(defaultOrg.username);
});

describe('secret redaction WITH env var (SF_TEMP_SHOW_SECRETS)', () => {
const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS';

beforeEach(() => {
process.env[SHOW_TOKENS_ENV] = 'true';
});

afterEach(() => {
delete process.env[SHOW_TOKENS_ENV];
});

it('shows the real access token and password', async () => {
await prepareStubs();
const result = await DisplayUserCommand.run(['--json', '--target-org', defaultOrg.username]);
expect(result.accessToken).to.equal(defaultOrg.accessToken);
expect(result.password).to.equal('-a098u234/1!@#');
});
});
});
22 changes: 20 additions & 2 deletions test/commands/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const expected = [
username: user1,
profileName: 'System Administrator',
orgId: 'abc123',
accessToken: 'accessToken',
accessToken: "[REDACTED] Use 'sf org auth show-access-token' to view",
instanceUrl,
loginUrl,
userId: '0052D0000043PbGQAU',
Expand All @@ -42,7 +42,7 @@ const expected = [
username: user2,
profileName: 'System Administrator',
orgId: 'abc123',
accessToken: 'accessToken',
accessToken: "[REDACTED] Use 'sf org auth show-access-token' to view",
instanceUrl,
loginUrl,
userId: '0052D0000043PcBQAU',
Expand Down Expand Up @@ -150,4 +150,22 @@ describe('org:list:users', () => {
const result = await ListUsersCommand.run(['--json', '--target-org', user1]);
expect(result).to.deep.equal(expected);
});

describe('secret redaction WITH env var (SF_TEMP_SHOW_SECRETS)', () => {
const SHOW_TOKENS_ENV = 'SF_TEMP_SHOW_SECRETS';

beforeEach(() => {
process.env[SHOW_TOKENS_ENV] = 'true';
});

afterEach(() => {
delete process.env[SHOW_TOKENS_ENV];
});

it('shows real access tokens when env var is set', async () => {
const result = await ListUsersCommand.run(['--json', '--target-org', user1]);
expect(result[0].accessToken).to.equal('accessToken');
expect(result[1].accessToken).to.equal('accessToken');
});
});
});
10 changes: 9 additions & 1 deletion test/commands/password/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ describe('org:generate:password', () => {
true,
'complexity 4 passwords have symbols'
);
expect(uxStubs.warn.args.flat()).to.have.length(0, 'no warnings expected');
const warnCalls = uxStubs.warn.args.flat();
expect(warnCalls).to.have.length(1, 'only the password security warning expected');
expect(warnCalls[0]).to.include('password');
});
});

Expand Down Expand Up @@ -233,4 +235,10 @@ describe('org:generate:password', () => {
expect(result.name).to.equal('NoSelfSetError');
}
});

it('viewWithCommand message references org auth show-user-password', () => {
const viewMsg = messages.getMessage('viewWithCommand', ['sf', 'testuser@example.com']);
expect(viewMsg).to.include('org auth show-user-password');
expect(viewMsg).to.not.include('org display user');
});
});
2 changes: 1 addition & 1 deletion test/forceCommands.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('verifies legacy force commands run successfully ', () => {

expect(output?.result.orgId).to.have.length(18);
expect(output?.result.id).to.have.length(18);
expect(output?.result?.accessToken?.startsWith(output?.result.orgId.substr(0, 15))).to.be.true;
expect(output?.result?.accessToken).to.include('[REDACTED]');
mainUserId = output?.result.id;
});

Expand Down
Loading