Skip to content
Open
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
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.sfdxAuthUrl

[REDACTED] Use 'sf org auth show-sfdx-auth-url' 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.
35 changes: 33 additions & 2 deletions src/commands/org/create/scratch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { MultiStageOutput } from '@oclif/multi-stage-output';
import {
envVars,
Lifecycle,
Messages,
Org,
Expand All @@ -26,14 +27,15 @@ import {
SfError,
} from '@salesforce/core';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { Duration } from '@salesforce/kit';
import { Duration, omit } from '@salesforce/kit';
import terminalLink from 'terminal-link';
import { capitalCase } from 'change-case';
import { buildScratchOrgRequest } from '../../../shared/scratchOrgRequest.js';
import { ScratchCreateResponse } from '../../../shared/orgTypes.js';

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

const definitionFileHelpGroupName = 'Definition File Override';

Expand Down Expand Up @@ -251,7 +253,36 @@ export default class OrgCreateScratch extends SfCommand<ScratchCreateResponse> {
this.logSuccess(messages.getMessage('success'));
}

return { username, scratchOrgInfo, authFields, warnings, orgId: authFields?.orgId };
// TODO: Remove env var workaround
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
const accessTokenRedacted = secretsMessages.getMessage('redacted.accessToken');

const redactedAuthFields = authFields
? {
...authFields,
accessToken: showSecretsEnvVarIsSet ? authFields.accessToken : accessTokenRedacted,
refreshToken: undefined,
clientSecret: undefined,
}
: undefined;

const redactedScratchOrgInfo = omit(scratchOrgInfo, ['AuthCode']);

if (this.jsonEnabled()) {
if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org create scratch --json']));
} else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org create scratch --json']));
}
}

return {
username,
scratchOrgInfo: redactedScratchOrgInfo,
authFields: redactedAuthFields,
warnings,
orgId: authFields?.orgId,
};
} catch (error) {
mso.error();
if (error instanceof SfError && error.name === 'ScratchOrgInfoTimeoutError') {
Expand Down
43 changes: 37 additions & 6 deletions src/commands/org/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
loglevel,
orgApiVersionFlagWithDeprecations,
} from '@salesforce/sf-plugins-core';
import { AuthInfo, Messages, Org, SfError, trimTo15 } from '@salesforce/core';
import { AuthInfo, envVars, Messages, Org, SfError, trimTo15 } from '@salesforce/core';
import { camelCaseToTitleCase } from '@salesforce/kit';
import { AuthFieldsFromFS, OrgDisplayReturn, ScratchOrgFields } from '../../shared/orgTypes.js';
import { getAliasByUsername } from '../../shared/utils.js';
Expand All @@ -31,6 +31,8 @@ import { OrgListUtil } from '../../shared/orgListUtil.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'display');
const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages');
const secretsMessages = Messages.loadMessages('@salesforce/plugin-org', 'secrets-redacted');

export class OrgDisplayCommand extends SfCommand<OrgDisplayReturn> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand All @@ -54,42 +56,71 @@ export class OrgDisplayCommand extends SfCommand<OrgDisplayReturn> {
this.org = flags['target-org'];
this.org.getConnection(flags['api-version']);
try {
// TODO: Once env var is removed, this refresh could be removed.
// the auth file might have a stale access token. We want to refresh it before getting the fields
await this.org.refreshAuth();
} catch (error) {
// even if this fails, we want to display the information we can read from the auth file
this.warn('unable to refresh auth for org');
}

const accessTokenRedactedMessage = secretsMessages.getMessage('redacted.accessToken');
const sfdxAuthUrlRedactedMessage = secretsMessages.getMessage('redacted.sfdxAuthUrl');
const passwordRedactedMessage = secretsMessages.getMessage('redacted.userPassword');

const envVarAsATempWorkaroundMessage = secretsMessages.getMessage('temp.envVarWorkaround', ['sf org display']);
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
const envVarIsSetWarning = secretsMessages.getMessage('temp.envVarIsSet', ['sf org display']);

// translate to alias if necessary
const authInfo = await AuthInfo.create({ username: this.org.getUsername() });
const fields = authInfo.getFields(true) as AuthFieldsFromFS;

const isScratchOrg = Boolean(fields.devHubUsername);
const scratchOrgInfo = isScratchOrg && fields.orgId ? await this.getScratchOrgInformation(fields) : {};

const getSfdxAuthUrlOutput = (): string | undefined => {
if (flags.verbose && fields.refreshToken) {
// TODO: Remove env var workaround
return showSecretsEnvVarIsSet ? authInfo.getSfdxAuthUrl() : sfdxAuthUrlRedactedMessage;
}
return undefined;
};

const returnValue: OrgDisplayReturn = {
// renamed properties
id: fields.orgId,
devHubId: fields.devHubUsername,

// copied properties
apiVersion: fields.instanceApiVersion,
accessToken: fields.accessToken,
// Access token will always exist, don't check for value first
// TODO: Remove env var workaround
accessToken: showSecretsEnvVarIsSet ? fields.accessToken : accessTokenRedactedMessage,
instanceUrl: fields.instanceUrl,
username: fields.username,
clientId: fields.clientId,
password: fields.password,
// Password only exists if it was generated locally, check for value first
// TODO: Remove env var workaround
password: fields.password ? (showSecretsEnvVarIsSet ? fields.password : passwordRedactedMessage) : undefined,
...scratchOrgInfo,

// properties with more complex logic
connectedStatus: isScratchOrg
? undefined
: await OrgListUtil.determineConnectedStatusForNonScratchOrg(fields.username),
sfdxAuthUrl: flags.verbose && fields.refreshToken ? authInfo.getSfdxAuthUrl() : undefined,
sfdxAuthUrl: getSfdxAuthUrlOutput(),
alias: await getAliasByUsername(fields.username),
// TODO: deprecate these values
clientApps: fields.clientApps ? Object.keys(fields.clientApps).join(',') : undefined,
};
this.warn(sharedMessages.getMessage('SecurityWarning'));

if (showSecretsEnvVarIsSet) {
this.warn(envVarIsSetWarning);
this.warn(sharedMessages.getMessage('SecurityWarning'));
} else {
this.warn(envVarAsATempWorkaroundMessage);
}

this.print(returnValue);
return returnValue;
}
Expand Down
24 changes: 23 additions & 1 deletion src/commands/org/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@

import { EOL } from 'node:os';
import { Flags, loglevel, SfCommand } from '@salesforce/sf-plugins-core';
import { AuthInfo, ConfigAggregator, ConfigInfo, Connection, Org, SfError, Messages, Logger } from '@salesforce/core';
import {
AuthInfo,
ConfigAggregator,
ConfigInfo,
Connection,
envVars,
Org,
SfError,
Messages,
Logger,
} from '@salesforce/core';
import { Interfaces } from '@oclif/core';
import ansis, { type Ansis } from 'ansis';
import { OrgListUtil, identifyActiveOrgByStatus } from '../../shared/orgListUtil.js';
Expand All @@ -25,6 +35,7 @@ import { ExtendedAuthFields, FullyPopulatedScratchOrgFields } from '../../shared

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

export const defaultOrgEmoji = '🍁';
export const defaultHubEmoji = '🌳';
Expand Down Expand Up @@ -126,6 +137,17 @@ Legend: ${defaultHubEmoji}=Default DevHub, ${defaultOrgEmoji}=Default Org ${
}`
);

// TODO: Remove after env var workaround is removed
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);

if (this.jsonEnabled()) {
if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org list']));
} else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org list']));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
could it print the warning all the time instead of only for --json?
it's an old command so most --json calls are likely being piped to jq or something else, users updating to latest and running sf org list in their local machines could notice the message earlier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HRO doesn't show tokens though. I think it would be confusing to have a warning about redacted messages when none are displayed.
My thought was that if someone is piping an AT from org list, it will fail because the accessToken will now be the redacted message. They will then likely see the warnings since warnings are sent to stderr

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh right, forgot it's actually a breaking change. Nvm 👍🏼

}
}

return result;
}

Expand Down
35 changes: 34 additions & 1 deletion src/commands/org/resume/scratch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { strict as assert } from 'node:assert';

import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import {
envVars,
Lifecycle,
Messages,
ScratchOrgCache,
Expand All @@ -30,10 +31,12 @@ import {
import terminalLink from 'terminal-link';
import { MultiStageOutput } from '@oclif/multi-stage-output';
import { capitalCase } from 'change-case';
import { omit } from '@salesforce/kit';
import { ScratchCreateResponse } from '../../../shared/orgTypes.js';

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

export default class OrgResumeScratch extends SfCommand<ScratchCreateResponse> {
public static readonly summary = messages.getMessage('summary');
Expand Down Expand Up @@ -127,7 +130,37 @@ export default class OrgResumeScratch extends SfCommand<ScratchCreateResponse> {

this.log();
this.logSuccess(messages.getMessage('success'));
return { username, scratchOrgInfo, authFields, warnings, orgId: authFields?.orgId };

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

const redactedAuthFields = authFields
? {
...authFields,
accessToken: showSecretsEnvVarIsSet ? authFields.accessToken : accessTokenRedacted,
refreshToken: undefined,
clientSecret: undefined,
}
: undefined;

const redactedScratchOrgInfo = scratchOrgInfo ? omit(scratchOrgInfo, ['AuthCode']) : undefined;

if (this.jsonEnabled()) {
if (showSecretsEnvVarIsSet) {
this.warn(secretsMessages.getMessage('temp.envVarIsSet', ['sf org resume scratch --json']));
} else {
this.warn(secretsMessages.getMessage('temp.envVarWorkaround', ['sf org resume scratch --json']));
}
}

return {
username,
scratchOrgInfo: redactedScratchOrgInfo,
authFields: redactedAuthFields,
warnings,
orgId: authFields?.orgId,
};
} catch (e) {
mso.error();

Expand Down
32 changes: 26 additions & 6 deletions src/shared/orgListUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import fs from 'node:fs/promises';
import {
Org,
AuthInfo,
envVars,
Global,
Logger,
Messages,
SfError,
trimTo15,
ConfigAggregator,
Expand All @@ -38,6 +40,9 @@ import type {
AuthFieldsFromFS,
} from './orgTypes.js';

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

type OrgGroups = {
nonScratchOrgs: ExtendedAuthFields[];
scratchOrgs: ExtendedAuthFieldsScratch[];
Expand Down Expand Up @@ -101,12 +106,27 @@ export class OrgListUtil {
OrgListUtil.processScratchOrgs(orgs.scratchOrgs),
]);

// TODO: Remove env var workaround
const showSecretsEnvVarIsSet = envVars.getBoolean('SF_TEMP_SHOW_SECRETS', false);
const redactSecrets = <T extends { accessToken?: string; password?: string }>(org: T): T => ({
...org,
accessToken: showSecretsEnvVarIsSet ? org.accessToken : secretsMessages.getMessage('redacted.accessToken'),
password: org.password
? showSecretsEnvVarIsSet
? org.password
: secretsMessages.getMessage('redacted.userPassword')
: undefined,
});

const redactedNonScratchOrgs = nonScratchOrgs.map(redactSecrets);
const redactedScratchOrgs = scratchOrgs.map(redactSecrets);

return {
nonScratchOrgs,
scratchOrgs,
sandboxes: nonScratchOrgs.filter(sandboxFilter),
other: nonScratchOrgs.filter(regularOrgFilter),
devHubs: nonScratchOrgs.filter(devHubFilter),
nonScratchOrgs: redactedNonScratchOrgs,
scratchOrgs: redactedScratchOrgs,
sandboxes: redactedNonScratchOrgs.filter(sandboxFilter),
other: redactedNonScratchOrgs.filter(regularOrgFilter),
devHubs: redactedNonScratchOrgs.filter(devHubFilter),
};
}

Expand Down Expand Up @@ -404,7 +424,7 @@ const authErrorHandler = async (err: unknown, username: string): Promise<string>
const logger = await OrgListUtil.retrieveLogger();
logger.trace(`error refreshing auth for org: ${username}`);
logger.trace(error);
// Orgs under maintenace return html as the error message.
// Orgs under maintenance return html as the error message.
if (error.message.includes('maintenance')) return 'Down (Maintenance)';
// handle other potential html responses
if (error.message.includes('<html>') || error.message.includes('<!DOCTYPE HTML>')) return 'Bad Response';
Expand Down
40 changes: 40 additions & 0 deletions test/shared/orgListUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,46 @@ describe('orgListUtil tests', () => {
expect(orgGroups.nonScratchOrgs[0].connectedStatus).to.equal('bad file');
expect(checkNonScratchOrgIsDevHub.called).to.be.false;
});

describe('secret redaction', () => {
it('redacts access tokens in nonScratchOrgs', async () => {
const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false);
expect(orgs.nonScratchOrgs[0].accessToken).to.include('[REDACTED]');
});

it('redacts access tokens in scratchOrgs', async () => {
const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false);
for (const org of orgs.scratchOrgs) {
expect(org.accessToken).to.include('[REDACTED]');
}
});

it('redacts access tokens in devHubs', async () => {
const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false);
for (const org of orgs.devHubs) {
expect(org.accessToken).to.include('[REDACTED]');
}
});
});

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 in scratchOrgs', async () => {
const orgs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, false);
for (const org of orgs.scratchOrgs) {
expect(org.accessToken).to.not.include('[REDACTED]');
}
});
});
});

describe('auth file reading tests', () => {
Expand Down
Loading
Loading