Skip to content

Commit 62c3c5f

Browse files
author
childish-sambino
authored
feat: improve 'access denied' error messaging (twilio#95)
When users attempt to access resources that require auth greater than what Standard API Keys grant, they are met with an access denied error. Since CLI profiles use such keys, messaging has been added instructing how to use non-standard auth when working with such resources.
1 parent 91a1898 commit 62c3c5f

File tree

3 files changed

+86
-71
lines changed

3 files changed

+86
-71
lines changed

src/base-commands/twilio-client-command.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ const { TwilioApiClient, TwilioApiFlags } = require('../services/twilio-api');
55
const { TwilioCliError } = require('../services/error');
66
const { translateValues } = require('../services/javascript-utilities');
77
const { camelCase, kebabCase } = require('../services/naming-conventions');
8-
const { HELP_ENVIRONMENT_VARIABLES } = require('../services/messaging/help-messages');
8+
const { ACCESS_DENIED, HELP_ENVIRONMENT_VARIABLES } = require('../services/messaging/help-messages');
99

1010
// CLI flags are kebab-cased, whereas API flags are PascalCased.
1111
const CliFlags = translateValues(TwilioApiFlags, kebabCase);
1212

13+
const ACCESS_DENIED_CODE = 20003;
14+
1315
class TwilioClientCommand extends BaseCommand {
1416
constructor(argv, config) {
1517
super(argv, config);
@@ -51,6 +53,18 @@ class TwilioClientCommand extends BaseCommand {
5153
this.httpClient = new CliRequestClient(this.id, this.logger);
5254
}
5355

56+
async catch(error) {
57+
// Append to the error message when catching API access denied errors with
58+
// profile-auth (i.e., standard API key auth).
59+
if (error instanceof TwilioCliError && error.exitCode === ACCESS_DENIED_CODE) {
60+
if (!this.currentProfile.id.startsWith('${TWILIO')) { // Auth *not* using env vars.
61+
error.message += '\n\n' + ACCESS_DENIED;
62+
}
63+
}
64+
65+
return super.catch(error);
66+
}
67+
5468
parseProperties() {
5569
if (!this.constructor.PropertyFlags) {
5670
return null;

src/services/messaging/help-messages.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
const { CLI_NAME } = require('../config');
22

33
const ENV_VAR_CMD = process.platform === 'win32' ? 'set' : 'export';
4-
5-
exports.HELP_ENVIRONMENT_VARIABLES = `Alternatively, ${CLI_NAME} can use credentials stored in environment variables:
6-
7-
# OPTION 1 (recommended)
4+
const ENV_VARS_USAGE = `# OPTION 1 (recommended)
85
${ENV_VAR_CMD} TWILIO_ACCOUNT_SID=your Account SID from twil.io/console
96
${ENV_VAR_CMD} TWILIO_API_KEY=an API Key created at twil.io/get-api-key
107
${ENV_VAR_CMD} TWILIO_API_SECRET=the secret for the API Key
118
129
# OPTION 2
1310
${ENV_VAR_CMD} TWILIO_ACCOUNT_SID=your Account SID from twil.io/console
14-
${ENV_VAR_CMD} TWILIO_AUTH_TOKEN=your Auth Token from twil.io/console
11+
${ENV_VAR_CMD} TWILIO_AUTH_TOKEN=your Auth Token from twil.io/console`;
12+
13+
exports.HELP_ENVIRONMENT_VARIABLES = `Alternatively, ${CLI_NAME} can use credentials stored in environment variables:
14+
15+
${ENV_VARS_USAGE}
16+
17+
Once these environment variables are set, a ${CLI_NAME} profile is not required and you may skip the "login" step.`;
18+
19+
exports.ACCESS_DENIED = `${CLI_NAME} profiles use Standard API Keys which are not permitted to manage Accounts (e.g., create Subaccounts) and other API Keys. If you require this functionality a Master API Key or Auth Token must be stored in environment variables:
1520
16-
Once these environment variables are set, a ${CLI_NAME} profile is not required to move forward with installation.`;
21+
${ENV_VARS_USAGE}`;
1722

1823
exports.NETWORK_ERROR = `${CLI_NAME} encountered a network connectivity error. \
1924
Please check your network connection and try your command again. \

test/base-commands/twilio-client-command.test.js

+60-64
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,66 @@
11
const { expect, test, constants } = require('@twilio/cli-test');
22
const TwilioClientCommand = require('../../src/base-commands/twilio-client-command');
33
const { Config, ConfigData } = require('../../src/services/config');
4-
5-
const ORIGINAL_ENV = process.env;
4+
const { TwilioCliError } = require('../../src/services/error');
65

76
describe('base-commands', () => {
87
describe('twilio-client-command', () => {
98
class TestClientCommand extends TwilioClientCommand {
109
}
1110

12-
class ThrowingClientCommand extends TwilioClientCommand {
11+
class ThrowingUnknownClientCommand extends TwilioClientCommand {
1312
async run() {
1413
await super.run();
1514

1615
throw new Error('We were so wrong!');
1716
}
1817
}
1918

19+
class Throwing20003ClientCommand extends TwilioClientCommand {
20+
async run() {
21+
await super.run();
22+
23+
throw new TwilioCliError('Access Denied!', 20003);
24+
}
25+
}
26+
2027
class AccountSidClientCommand extends TwilioClientCommand {
2128
}
2229

2330
TestClientCommand.flags = TwilioClientCommand.flags;
24-
ThrowingClientCommand.flags = TwilioClientCommand.flags;
31+
ThrowingUnknownClientCommand.flags = TwilioClientCommand.flags;
32+
Throwing20003ClientCommand.flags = TwilioClientCommand.flags;
2533
AccountSidClientCommand.flags = Object.assign({}, TwilioClientCommand.flags, TwilioClientCommand.accountSidFlag);
2634

2735
const setUpTest = (
2836
args = [],
29-
{ setUpUserConfig = undefined, mockSecureStorage = true, commandClass: CommandClass = TestClientCommand } = {}
37+
{
38+
setUpUserConfig = undefined,
39+
mockSecureStorage = true,
40+
commandClass: CommandClass = TestClientCommand,
41+
envRegion, envEdge, configRegion = 'configRegion', configEdge
42+
} = {}
3043
) => {
3144
return test
3245
.do(ctx => {
3346
ctx.userConfig = new ConfigData();
47+
ctx.userConfig.edge = configEdge;
48+
49+
if (envRegion) {
50+
process.env.TWILIO_REGION = envRegion;
51+
process.env.TWILIO_ACCOUNT_SID = constants.FAKE_ACCOUNT_SID;
52+
process.env.TWILIO_AUTH_TOKEN = constants.FAKE_API_SECRET;
53+
}
54+
55+
if (envEdge) {
56+
process.env.TWILIO_EDGE = envEdge;
57+
}
58+
3459
if (setUpUserConfig) {
3560
setUpUserConfig(ctx.userConfig);
3661
} else {
3762
ctx.userConfig.addProfile('MyFirstProfile', constants.FAKE_ACCOUNT_SID);
38-
ctx.userConfig.addProfile('twilio-cli-unit-testing', constants.FAKE_ACCOUNT_SID, 'stage');
63+
ctx.userConfig.addProfile('region-edge-testing', constants.FAKE_ACCOUNT_SID, configRegion);
3964
}
4065
})
4166
.twilioCliEnv(Config)
@@ -100,27 +125,41 @@ describe('base-commands', () => {
100125
expect(ctx.stderr).to.contain('TWILIO_ACCOUNT_SID');
101126
});
102127

103-
setUpTest(['-p', 'twilio-cli-unit-testing']).it('should create a client for a non-default profile', ctx => {
128+
setUpTest(['-p', 'region-edge-testing']).it('should create a client for a non-default profile', ctx => {
104129
expect(ctx.testCmd.twilioClient.accountSid).to.equal(constants.FAKE_ACCOUNT_SID);
105130
expect(ctx.testCmd.twilioClient.username).to.equal(constants.FAKE_API_KEY);
106-
expect(ctx.testCmd.twilioClient.password).to.equal(constants.FAKE_API_SECRET + 'twilio-cli-unit-testing');
107-
expect(ctx.testCmd.twilioClient.region).to.equal('stage');
131+
expect(ctx.testCmd.twilioClient.password).to.equal(constants.FAKE_API_SECRET + 'region-edge-testing');
132+
expect(ctx.testCmd.twilioClient.region).to.equal('configRegion');
108133
});
109134

110-
setUpTest(['-p', 'twilio-cli-unit-testing'], { mockSecureStorage: false })
135+
setUpTest(['-p', 'region-edge-testing'], { mockSecureStorage: false })
111136
.exit(1)
112137
.it('should handle a secure storage error', ctx => {
113-
expect(ctx.stderr).to.contain('Could not get credentials for profile "twilio-cli-unit-testing"');
138+
expect(ctx.stderr).to.contain('Could not get credentials for profile "region-edge-testing"');
114139
expect(ctx.stderr).to.contain('To reconfigure the profile, run:');
115-
expect(ctx.stderr).to.contain('twilio profiles:create --profile "twilio-cli-unit-testing"');
140+
expect(ctx.stderr).to.contain('twilio profiles:create --profile "region-edge-testing"');
116141
});
117142

118-
setUpTest([], { commandClass: ThrowingClientCommand })
143+
setUpTest([], { commandClass: ThrowingUnknownClientCommand })
119144
.exit(1)
120145
.it('should catch unhandled errors', ctx => {
121146
expect(ctx.stderr).to.contain('unexpected error');
122147
});
123148

149+
setUpTest([], { commandClass: Throwing20003ClientCommand })
150+
.exit(20003)
151+
.it('should catch access denied errors and enhance the message', ctx => {
152+
expect(ctx.stderr).to.contain('Access Denied');
153+
expect(ctx.stderr).to.contain('Standard API Keys');
154+
});
155+
156+
setUpTest([], { commandClass: Throwing20003ClientCommand, envRegion: 'region' })
157+
.exit(20003)
158+
.it('should catch access denied errors but not enhance the message when using env var auth', ctx => {
159+
expect(ctx.stderr).to.contain('Access Denied');
160+
expect(ctx.stderr).to.not.contain('Standard API Keys');
161+
});
162+
124163
describe('parseProperties', () => {
125164
setUpTest().it('should ignore empty PropertyFlags', ctx => {
126165
const updatedProperties = ctx.testCmd.parseProperties();
@@ -236,69 +275,26 @@ describe('base-commands', () => {
236275
});
237276

238277
describe('regional and edge support', () => {
239-
const envTest = (
240-
args = [],
241-
{ envRegion, envEdge, configRegion = 'configRegion', configEdge } = {}
242-
) => {
243-
return test
244-
.do(ctx => {
245-
ctx.userConfig = new ConfigData();
246-
ctx.userConfig.edge = configEdge;
247-
248-
if (envRegion) {
249-
process.env.TWILIO_REGION = envRegion;
250-
process.env.TWILIO_ACCOUNT_SID = constants.FAKE_ACCOUNT_SID;
251-
process.env.TWILIO_AUTH_TOKEN = constants.FAKE_API_SECRET;
252-
}
253-
if (envEdge) {
254-
process.env.TWILIO_EDGE = envEdge;
255-
}
256-
257-
ctx.userConfig.addProfile('default-profile', constants.FAKE_ACCOUNT_SID);
258-
ctx.userConfig.addProfile('region-edge-testing', constants.FAKE_ACCOUNT_SID, configRegion);
259-
})
260-
.twilioCliEnv(Config)
261-
.do(async ctx => {
262-
ctx.testCmd = new TwilioClientCommand(args, ctx.fakeConfig);
263-
ctx.testCmd.secureStorage =
264-
{
265-
async getCredentials(profileId) {
266-
return {
267-
apiKey: constants.FAKE_API_KEY,
268-
apiSecret: constants.FAKE_API_SECRET + profileId
269-
};
270-
}
271-
};
272-
273-
// This is essentially what oclif does behind the scenes.
274-
try {
275-
await ctx.testCmd.run();
276-
} catch (error) {
277-
await ctx.testCmd.catch(error);
278-
}
279-
process.env = ORIGINAL_ENV;
280-
});
281-
};
282-
283-
envTest([], { configEdge: 'edge' }).it('should use the config edge when defined', ctx => {
278+
setUpTest([], { configEdge: 'edge' }).it('should use the config edge when defined', ctx => {
284279
expect(ctx.testCmd.twilioApiClient.edge).to.equal('edge');
285280
expect(ctx.testCmd.twilioApiClient.region).to.be.undefined;
286281
});
287282

288-
envTest(['-p', 'region-edge-testing']).it('should use the config region when defined', ctx => {
283+
setUpTest(['-p', 'region-edge-testing']).it('should use the config region when defined', ctx => {
289284
expect(ctx.testCmd.twilioApiClient.region).to.equal('configRegion');
290285
expect(ctx.testCmd.twilioApiClient.edge).to.be.undefined;
291286
});
292287

293-
envTest([], { envRegion: 'region' }).it('should use the env region over a config region', ctx => {
288+
setUpTest([], { envRegion: 'region' }).it('should use the env region over a config region', ctx => {
294289
expect(ctx.testCmd.twilioApiClient.region).to.equal('region');
295290
expect(ctx.testCmd.twilioApiClient.edge).to.be.undefined;
296291
});
297292

298-
envTest([], { configEdge: 'configEdge', envEdge: 'edge', envRegion: 'region' }).it('should use the env edge over a config edge', ctx => {
299-
expect(ctx.testCmd.twilioApiClient.edge).to.equal('edge');
300-
expect(ctx.testCmd.twilioApiClient.region).to.equal('region');
301-
});
293+
setUpTest([], { configEdge: 'configEdge', envEdge: 'edge', envRegion: 'region' })
294+
.it('should use the env edge over a config edge', ctx => {
295+
expect(ctx.testCmd.twilioApiClient.edge).to.equal('edge');
296+
expect(ctx.testCmd.twilioApiClient.region).to.equal('region');
297+
});
302298
});
303299
});
304300
});

0 commit comments

Comments
 (0)