From 5d403916214fd8cb8f08b45de08e1a934b97cd3a Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 19 May 2026 10:20:39 -0500 Subject: [PATCH 1/2] feat: remove sensitive output --- messages/display.md | 2 +- messages/password.generate.md | 4 +- messages/secrets-redacted.md | 19 +++++++ src/baseCommands/user/password/generate.ts | 2 + src/commands/org/create/user.ts | 6 ++- src/commands/org/display/user.ts | 20 +++++-- src/commands/org/list/users.ts | 19 ++++++- test/commands/display.test.ts | 61 +++++++++++++--------- test/commands/list.test.ts | 22 +++++++- test/commands/password/generate.test.ts | 10 +++- 10 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 messages/secrets-redacted.md diff --git a/messages/display.md b/messages/display.md index 67302d60..5a782854 100644 --- a/messages/display.md +++ b/messages/display.md @@ -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 diff --git a/messages/password.generate.md b/messages/password.generate.md index a3a3e9bd..8138f64f 100644 --- a/messages/password.generate.md +++ b/messages/password.generate.md @@ -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 @@ -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 diff --git a/messages/secrets-redacted.md b/messages/secrets-redacted.md new file mode 100644 index 00000000..80c0fa1c --- /dev/null +++ b/messages/secrets-redacted.md @@ -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. diff --git a/src/baseCommands/user/password/generate.ts b/src/baseCommands/user/password/generate.ts index 3cf483e2..e88d91f7 100644 --- a/src/baseCommands/user/password/generate.ts +++ b/src/baseCommands/user/password/generate.ts @@ -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; @@ -90,6 +91,7 @@ export abstract class UserPasswordGenerateBaseCommand extends SfCommand { value: pass.toString(), }); }); + this.warn(secretsMessages.getMessage('warning.passwordGenerated')); } catch (error) { const err = error as SfError; this.failures.push({ @@ -228,8 +230,8 @@ export class CreateUserCommand extends SfCommand { // @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']) diff --git a/src/commands/org/display/user.ts b/src/commands/org/display/user.ts index 8c75e429..05fccc8e 100644 --- a/src/commands/org/display/user.ts +++ b/src/commands/org/display/user.ts @@ -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, @@ -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; @@ -95,8 +96,13 @@ export class DisplayUserCommand extends SfCommand { ); } + // 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, @@ -112,10 +118,16 @@ export class DisplayUserCommand extends SfCommand { } 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); diff --git a/src/commands/org/list/users.ts b/src/commands/org/list/users.ts index 68b4ab9a..0f8ec17b 100644 --- a/src/commands/org/list/users.ts +++ b/src/commands/org/list/users.ts @@ -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, @@ -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; @@ -68,6 +69,10 @@ export class ListUsersCommand extends SfCommand { (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. @@ -80,7 +85,7 @@ export class ListUsersCommand extends SfCommand { 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, @@ -96,6 +101,16 @@ export class ListUsersCommand extends SfCommand { })), 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; } } diff --git a/test/commands/display.test.ts b/test/commands/display.test.ts index ae2be7a7..a428b39a 100644 --- a/test/commands/display.test.ts +++ b/test/commands/display.test.ts @@ -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!@#'); + }); }); }); diff --git a/test/commands/list.test.ts b/test/commands/list.test.ts index 117a4797..3fa61a8a 100644 --- a/test/commands/list.test.ts +++ b/test/commands/list.test.ts @@ -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', @@ -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', @@ -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'); + }); + }); }); diff --git a/test/commands/password/generate.test.ts b/test/commands/password/generate.test.ts index 1d4f79a3..f8202739 100644 --- a/test/commands/password/generate.test.ts +++ b/test/commands/password/generate.test.ts @@ -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'); }); }); @@ -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'); + }); }); From 231d5b3a6bbe35fee44599e7fccd13d28a1d1bb3 Mon Sep 17 00:00:00 2001 From: Eric Willhoit Date: Tue, 19 May 2026 17:17:31 -0500 Subject: [PATCH 2/2] chore: nuts --- test/allCommands.nut.ts | 2 +- test/forceCommands.nut.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/allCommands.nut.ts b/test/allCommands.nut.ts index 2cff62cf..b596e692 100644 --- a/test/allCommands.nut.ts +++ b/test/allCommands.nut.ts @@ -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; }); diff --git a/test/forceCommands.nut.ts b/test/forceCommands.nut.ts index 59a8e727..f3a7ed74 100644 --- a/test/forceCommands.nut.ts +++ b/test/forceCommands.nut.ts @@ -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; });