diff --git a/messages/messages.json b/messages/messages.json index 34c48932..408ff264 100644 --- a/messages/messages.json +++ b/messages/messages.json @@ -24,5 +24,7 @@ "setAlias": "set an alias for the authenticated org", "instanceUrl": "the login URL of the instance the org lives on", "authorizeCommandSuccess": "Successfully authorized %s with org ID %s", - "authorizeCommandCloseBrowser": "You may now close the browser" + "authorizeCommandCloseBrowser": "You may now close the browser", + "warnAuth": "Logging in to a business or production org is not recommended on a demo or shared machine. Please run \"sfdx force:auth:logout --targetusername --noprompt\" when finished using this org, which is similar to logging out of the org in the browser.\n\nDo you want to authorize this org, %s, for use with the Salesforce CLI (y/n)?", + "noPromptAuth": "do not prompt for auth confirmation in demo mode" } diff --git a/src/commands/auth/jwt/grant.ts b/src/commands/auth/jwt/grant.ts index 41007b7e..5f490f37 100644 --- a/src/commands/auth/jwt/grant.ts +++ b/src/commands/auth/jwt/grant.ts @@ -8,6 +8,7 @@ import * as os from 'os'; import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; import { AuthFields, AuthInfo, AuthRemover, Messages, SfdxError } from '@salesforce/core'; +import { Prompts } from '../../../prompts'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-auth', 'jwt.grant'); @@ -48,11 +49,19 @@ export default class Grant extends SfdxCommand { char: 'a', description: commonMessages.getMessage('setAlias'), }), + noprompt: flags.boolean({ + char: 'p', + description: commonMessages.getMessage('noPromptAuth'), + required: false, + hidden: true, + }), }; public async run(): Promise { let result: AuthFields = {}; + if (await Prompts.shouldExitCommand(this.ux, this.flags.noprompt)) return {}; + try { const authInfo = await this.initAuthInfo(); diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts index 64fff9dd..f6591c4f 100644 --- a/src/commands/auth/logout.ts +++ b/src/commands/auth/logout.ts @@ -8,6 +8,7 @@ import * as os from 'os'; import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; import { AuthConfigs, AuthRemover, Global, Messages, Mode, SfdxError } from '@salesforce/core'; +import { Prompts } from '../../prompts'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-auth', 'logout'); @@ -58,12 +59,8 @@ export default class Logout extends SfdxCommand { } private async shouldRunCommand(authConfigs: AuthConfigs): Promise { - if (this.flags.noprompt || Global.getEnvironmentMode() === Mode.DEMO) { - return true; - } else { - const orgsToDelete = [[...authConfigs.keys()].join(os.EOL)]; - const answer = await this.ux.prompt(messages.getMessage('logoutCommandYesNo', orgsToDelete)); - return answer.toUpperCase() === 'YES' || answer.toUpperCase() === 'Y'; - } + const orgsToDelete = [[...authConfigs.keys()].join(os.EOL)]; + const message = messages.getMessage('logoutCommandYesNo', orgsToDelete); + return Prompts.shouldRunCommand(this.ux, this.flags.noprompt, message); } } diff --git a/src/commands/auth/sfdxurl/store.ts b/src/commands/auth/sfdxurl/store.ts index ee7edd9a..97ea46ea 100644 --- a/src/commands/auth/sfdxurl/store.ts +++ b/src/commands/auth/sfdxurl/store.ts @@ -8,6 +8,7 @@ import * as os from 'os'; import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command'; import { AuthFields, AuthInfo, fs, Messages } from '@salesforce/core'; +import { Prompts } from '../../../prompts'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-auth', 'sfdxurl.store'); @@ -37,9 +38,17 @@ export default class Store extends SfdxCommand { char: 'a', description: commonMessages.getMessage('setAlias'), }), + noprompt: flags.boolean({ + char: 'p', + description: commonMessages.getMessage('noPromptAuth'), + required: false, + hidden: true, + }), }; public async run(): Promise { + if (await Prompts.shouldExitCommand(this.ux, this.flags.noprompt)) return {}; + const sfdxAuthUrl = await fs.readFile(this.flags.sfdxurlfile, 'utf8'); const oauth2Options = AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl); const authInfo = await AuthInfo.create({ oauth2Options }); diff --git a/src/prompts.ts b/src/prompts.ts new file mode 100644 index 00000000..23639fff --- /dev/null +++ b/src/prompts.ts @@ -0,0 +1,39 @@ +/* + * 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 + */ +import { Messages, Global, Mode } from '@salesforce/core'; +import { UX } from '@salesforce/command'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-auth', 'messages'); + +export class Prompts { + public static async shouldExitCommand(ux: UX, noPrompt?: boolean, message?: string): Promise { + if (noPrompt || Global.getEnvironmentMode() !== Mode.DEMO) { + return false; + } else { + const answer = await ux.prompt(message || messages.getMessage('warnAuth')); + return Prompts.answeredNo(answer); + } + } + + public static async shouldRunCommand(ux: UX, noPrompt?: boolean, message?: string): Promise { + if (noPrompt || Global.getEnvironmentMode() === Mode.DEMO) { + return true; + } else { + const answer = await ux.prompt(message || messages.getMessage('warnAuth')); + return Prompts.answeredYes(answer); + } + } + + private static answeredYes(answer: string): boolean { + return ['YES', 'Y'].includes(answer.toUpperCase()); + } + + private static answeredNo(answer: string): boolean { + return !['YES', 'Y'].includes(answer.toUpperCase()); + } +} diff --git a/test/commands/auth/jwt/grant.test.ts b/test/commands/auth/jwt/grant.test.ts index 00777555..8e0119eb 100644 --- a/test/commands/auth/jwt/grant.test.ts +++ b/test/commands/auth/jwt/grant.test.ts @@ -9,6 +9,7 @@ import { $$, expect, test } from '@salesforce/command/lib/test'; import { AuthFields, AuthInfo, SfdxError } from '@salesforce/core'; import { MockTestOrgData } from '@salesforce/core/lib/testSetup'; import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import { UX } from '@salesforce/command'; interface Options { authInfoCreateFails?: boolean; @@ -46,7 +47,6 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs()) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '--json']) .it('should return auth fields', (ctx) => { const response = JSON.parse(ctx.stdout); @@ -109,7 +109,6 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs()) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '-s', '--json']) .it('should set defaultusername to username when -s is provided', (ctx) => { const response = JSON.parse(ctx.stdout); @@ -158,7 +157,6 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs()) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '-d', '--json']) .it('should set defaultdevhubusername to username when -d is provided', (ctx) => { const response = JSON.parse(ctx.stdout); @@ -237,7 +235,6 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs()) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '--json']) .it('should throw an error when client id (-i) is not provided', (ctx) => { const response = JSON.parse(ctx.stdout); @@ -248,7 +245,6 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs({ authInfoCreateFails: true })) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456INVALID', '--json']) .it('should throw an error when client id is invalid', (ctx) => { const response = JSON.parse(ctx.stdout); @@ -259,7 +255,6 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs()) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-i', '123456', '--json']) .it('should throw an error when private key file (-f) is not provided', (ctx) => { const response = JSON.parse(ctx.stdout); @@ -270,11 +265,63 @@ describe('auth:jwt:grant', async () => { test .do(async () => prepareStubs({ existingAuth: true })) .stdout() - // eslint-disable-next-line prettier/prettier .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '--json']) .it('should not throw an error when the authorization already exists', (ctx) => { const response = JSON.parse(ctx.stdout); expect(response.status).to.equal(0); expect(response.result).to.deep.equal(authFields); }); + + test + .do(async () => { + await prepareStubs(); + process.env['SFDX_ENV'] = 'demo'; + $$.SANDBOX.stub(UX.prototype, 'prompt').returns(Promise.resolve('yes')); + }) + .finally(() => { + delete process.env['SFDX_ENV']; + }) + .stdout() + .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '--json']) + .it('should auth when in demo mode (SFDX_ENV=demo) and prompt is answered with yes', (ctx) => { + const response = JSON.parse(ctx.stdout); + expect(response.status).to.equal(0); + expect(response.result).to.deep.equal(authFields); + expect(authInfoStub.save.callCount).to.equal(1); + }); + + test + .do(async () => { + await prepareStubs(); + process.env['SFDX_ENV'] = 'demo'; + $$.SANDBOX.stub(UX.prototype, 'prompt').returns(Promise.resolve('no')); + }) + .finally(() => { + delete process.env['SFDX_ENV']; + }) + .stdout() + .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '--json']) + .it('should do nothing when in demo mode (SFDX_ENV=demo) and prompt is answered with no', (ctx) => { + const response = JSON.parse(ctx.stdout); + expect(response.status).to.equal(0); + expect(response.result).to.deep.equal({}); + expect(authInfoStub.save.callCount).to.equal(0); + }); + + test + .do(async () => { + await prepareStubs(); + process.env['SFDX_ENV'] = 'demo'; + }) + .finally(() => { + delete process.env['SFDX_ENV']; + }) + .stdout() + .command(['auth:jwt:grant', '-u', testData.username, '-f', 'path/to/key.json', '-i', '123456', '--json', '-p']) + .it('should ignore prompt when in demo mode (SFDX_ENV=demo) and -p is provided', (ctx) => { + const response = JSON.parse(ctx.stdout); + expect(response.status).to.equal(0); + expect(response.result).to.deep.equal(authFields); + expect(authInfoStub.save.callCount).to.equal(1); + }); }); diff --git a/test/commands/auth/sfdxurl/store.test.ts b/test/commands/auth/sfdxurl/store.test.ts index f8ced392..81eefa34 100644 --- a/test/commands/auth/sfdxurl/store.test.ts +++ b/test/commands/auth/sfdxurl/store.test.ts @@ -9,6 +9,7 @@ import { $$, expect, test } from '@salesforce/command/lib/test'; import { AuthFields, AuthInfo, fs } from '@salesforce/core'; import { MockTestOrgData } from '@salesforce/core/lib/testSetup'; import { StubbedType, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import { UX } from '@salesforce/command'; interface Options { authInfoCreateFails?: boolean; @@ -173,4 +174,57 @@ describe('auth:sfdxurl:store', async () => { }, ]); }); + + test + .do(async () => { + await prepareStubs(); + process.env['SFDX_ENV'] = 'demo'; + $$.SANDBOX.stub(UX.prototype, 'prompt').returns(Promise.resolve('yes')); + }) + .finally(() => { + delete process.env['SFDX_ENV']; + }) + .stdout() + .command(['auth:sfdxurl:store', '-f', 'path/to/key.txt', '--json']) + .it('should auth when in demo mode (SFDX_ENV=demo) and prompt is answered with yes', (ctx) => { + const response = JSON.parse(ctx.stdout); + expect(response.status).to.equal(0); + expect(response.result).to.deep.equal(authFields); + expect(authInfoStub.save.callCount).to.equal(1); + }); + + test + .do(async () => { + await prepareStubs(); + process.env['SFDX_ENV'] = 'demo'; + $$.SANDBOX.stub(UX.prototype, 'prompt').returns(Promise.resolve('no')); + }) + .finally(() => { + delete process.env['SFDX_ENV']; + }) + .stdout() + .command(['auth:sfdxurl:store', '-f', 'path/to/key.txt', '--json']) + .it('should do nothing when in demo mode (SFDX_ENV=demo) and prompt is answered with no', (ctx) => { + const response = JSON.parse(ctx.stdout); + expect(response.status).to.equal(0); + expect(response.result).to.deep.equal({}); + expect(authInfoStub.save.callCount).to.equal(0); + }); + + test + .do(async () => { + await prepareStubs(); + process.env['SFDX_ENV'] = 'demo'; + }) + .finally(() => { + delete process.env['SFDX_ENV']; + }) + .stdout() + .command(['auth:sfdxurl:store', '-f', 'path/to/key.txt', '--json', '-p']) + .it('should ignore prompt when in demo mode (SFDX_ENV=demo) and -p is provided', (ctx) => { + const response = JSON.parse(ctx.stdout); + expect(response.status).to.equal(0); + expect(response.result).to.deep.equal(authFields); + expect(authInfoStub.save.callCount).to.equal(1); + }); });