Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add sandbox mode warning to amplify status #8078

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Object {
},
Object {
"apiKeyConfig": Object {
"apiKeyExpirationDate": undefined,
"apiKeyExpirationDays": undefined,
"description": undefined,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Object {
exports[`AppSyncAuthType to authConfig maps API_KEY correctly 1`] = `
Object {
"apiKeyConfig": Object {
"apiKeyExpirationDate": undefined,
"apiKeyExpirationDays": 120,
"description": undefined,
},
Expand Down Expand Up @@ -47,6 +48,7 @@ Object {

exports[`authConfig to AppSyncAuthType maps API_KEY auth correctly 1`] = `
Object {
"apiKeyExpirationDate": undefined,
"expirationTime": 120,
"keyDescription": "api key description",
"mode": "API_KEY",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
$TSContext,
open,
} from 'amplify-cli-core';
import { Duration, Expiration } from '@aws-cdk/core';

const serviceName = 'AppSync';
const elasticContainerServiceName = 'ElasticContainer';
Expand Down Expand Up @@ -508,9 +509,11 @@ export async function askAdditionalAuthQuestions(context, authConfig, defaultAut
if (await context.prompt.confirm('Configure additional auth types?')) {
// Get additional auth configured
const remainingAuthProviderChoices = authProviderChoices.filter(p => p.value !== defaultAuthType);
const currentAdditionalAuth = ((currentAuthConfig && currentAuthConfig.additionalAuthenticationProviders
? currentAuthConfig.additionalAuthenticationProviders
: []) as any[]).map(authProvider => authProvider.authenticationType);
const currentAdditionalAuth = (
(currentAuthConfig && currentAuthConfig.additionalAuthenticationProviders
? currentAuthConfig.additionalAuthenticationProviders
: []) as any[]
).map(authProvider => authProvider.authenticationType);

const additionalProvidersQuestion: CheckboxQuestion = {
type: 'checkbox',
Expand Down Expand Up @@ -619,6 +622,8 @@ async function askApiKeyQuestions() {
];

const apiKeyConfig = await inquirer.prompt(apiKeyQuestions);
const apiKeyExpirationDaysNum = Number(apiKeyConfig.apiKeyExpirationDays);
apiKeyConfig.apiKeyExpirationDate = Expiration.after(Duration.days(apiKeyExpirationDaysNum)).date;

return {
authenticationType: 'API_KEY',
Expand Down Expand Up @@ -677,9 +682,10 @@ function validateDays(input) {
}

function validateIssuerUrl(input) {
const isValid = /^(((?!http:\/\/(?!localhost))([a-zA-Z0-9.]{1,}):\/\/([a-zA-Z0-9-._~:?#@!$&'()*+,;=/]{1,})\/)|(?!http)(?!https)([a-zA-Z0-9.]{1,}):\/\/)$/.test(
input,
);
const isValid =
/^(((?!http:\/\/(?!localhost))([a-zA-Z0-9.]{1,}):\/\/([a-zA-Z0-9-._~:?#@!$&'()*+,;=/]{1,})\/)|(?!http)(?!https)([a-zA-Z0-9.]{1,}):\/\/)$/.test(
input,
);

if (!isValid) {
return 'The value must be a valid URI with a trailing forward slash. HTTPS must be used instead of HTTP unless you are using localhost.';
Expand Down Expand Up @@ -779,8 +785,8 @@ const buildPolicyResource = (resourceName: string, path: string | null) => {
{
Ref: `${category}${resourceName}GraphQLAPIIdOutput`,
},
...(path ? [path] : [])
]
...(path ? [path] : []),
],
],
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const authConfigToAppSyncAuthTypeMap: Record<string, (authConfig: any) => AppSyn
API_KEY: authConfig => ({
mode: 'API_KEY',
expirationTime: authConfig.apiKeyConfig.apiKeyExpirationDays,
apiKeyExpirationDate: authConfig.apiKeyConfig?.apiKeyExpirationDate,
keyDescription: authConfig.apiKeyConfig.description,
}),
AWS_IAM: () => ({
Expand All @@ -54,6 +55,7 @@ const appSyncAuthTypeToAuthConfigMap: Record<string, (authType: AppSyncAuthType)
authenticationType: 'API_KEY',
apiKeyConfig: {
apiKeyExpirationDays: authType.expirationTime,
apiKeyExpirationDate: authType?.apiKeyExpirationDate,
description: authType.keyDescription,
},
}),
Expand Down
5 changes: 2 additions & 3 deletions packages/amplify-cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ interface AmplifyToolkit {
) => $TSAny;
sharedQuestions: () => $TSAny;
showAllHelp: () => $TSAny;
showGlobalSandboxModeWarning: (context: $TSContext) => $TSAny;
showHelp: (header: string, commands: { name: string; description: string }[]) => $TSAny;
showHelpfulProviderLinks: (context: $TSContext) => $TSAny;
showResourceTable: () => $TSAny;
Expand Down Expand Up @@ -310,9 +311,7 @@ interface AmplifyToolkit {
leaveBreadcrumbs: (category: string, resourceName: string, breadcrumbs: unknown) => void;
readBreadcrumbs: (category: string, resourceName: string) => $TSAny;
loadRuntimePlugin: (context: $TSContext, pluginId: string) => Promise<$TSAny>;
getImportedAuthProperties: (
context: $TSContext,
) => {
getImportedAuthProperties: (context: $TSContext) => {
imported: boolean;
userPoolId?: string;
authRoleArn?: string;
Expand Down
15 changes: 4 additions & 11 deletions packages/amplify-cli/src/__tests__/commands/status.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { UnknownArgumentError } from 'amplify-cli-core';

describe('amplify status: ', () => {
const { run } = require('../../commands/status');
const runStatusCmd = run;
Expand All @@ -11,17 +9,10 @@ describe('amplify status: ', () => {
});

it('status run method should call context.amplify.showStatusTable', async () => {
const cliInput = {
command: 'status',
subCommands: [],
options: {
verbose: true,
},
};

const mockContextNoCLArgs = {
amplify: {
showStatusTable: jest.fn(),
showGlobalSandboxModeWarning: jest.fn(),
showHelpfulProviderLinks: jest.fn(),
getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: mockPath }),
},
Expand All @@ -43,6 +34,7 @@ describe('amplify status: ', () => {
const mockContextWithVerboseOptionAndCLArgs = {
amplify: {
showStatusTable: jest.fn(),
showGlobalSandboxModeWarning: jest.fn(),
showHelpfulProviderLinks: jest.fn(),
getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: statusPluginInfo }),
},
Expand All @@ -61,6 +53,7 @@ describe('amplify status: ', () => {
const mockContextWithVerboseOptionWithCategoriesAndCLArgs = {
amplify: {
showStatusTable: jest.fn(),
showGlobalSandboxModeWarning: jest.fn(),
showHelpfulProviderLinks: jest.fn(),
getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: statusPluginInfo }),
},
Expand All @@ -82,6 +75,7 @@ describe('amplify status: ', () => {
const mockContextWithHelpSubcommandAndCLArgs = {
amplify: {
showStatusTable: jest.fn(),
showGlobalSandboxModeWarning: jest.fn(),
showHelpfulProviderLinks: jest.fn(),
getCategoryPluginInfo: jest.fn().mockReturnValue({ packageLocation: statusPluginInfo }),
},
Expand All @@ -94,5 +88,4 @@ describe('amplify status: ', () => {
//TBD: to move ViewResourceTableParams into a separate file for mocking instance functions.
expect(mockContextWithHelpSubcommandAndCLArgs.amplify.showStatusTable.mock.calls.length).toBe(0);
});

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import fs from 'fs';
import { getAppSyncApiConfig, getApiKeyConfig, apiKeyIsActive, hasApiKey } from '../../../extensions/amplify-helpers/get-api-key-config';
import { stateManager } from 'amplify-cli-core';

let amplifyMeta;

jest.mock('amplify-cli-core', () => {
const original = jest.requireActual('amplify-cli-core');
return {
...original,
stateManager: {
metaFileExists: jest.fn(),
getMeta: jest.fn().mockImplementation(() => JSON.parse(amplifyMeta.toString())),
},
};
});

const stateManager_mock = stateManager as jest.Mocked<typeof stateManager>;

describe('getAppSyncApiConfig', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`);
});

it('returns the api object', async () => {
const result = getAppSyncApiConfig();

expect(result).toStrictEqual({
service: 'AppSync',
providerPlugin: 'awscloudformation',
output: {
authConfig: {
defaultAuthentication: {
authenticationType: 'AWS_IAM',
},
additionalAuthenticationProviders: [
{
authenticationType: 'API_KEY',
apiKeyConfig: {
apiKeyExpirationDays: 2,
apiKeyExpirationDate: '2021-08-20T20:38:07.585Z',
description: '',
},
},
],
},
globalSandboxModeConfig: {
dev: {
enabled: true,
},
},
},
});
});
});

describe('getApiKeyConfig', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`);
});

it('returns the api key config', () => {
const result = getApiKeyConfig();

expect(result).toStrictEqual({
apiKeyExpirationDays: 2,
apiKeyExpirationDate: '2021-08-20T20:38:07.585Z',
description: '',
});
});
});

describe('apiKeyIsActive', () => {
describe('with expired key', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`);
});

it('returns false', () => {
expect(apiKeyIsActive()).toBe(false);
});
});

describe('with no api key config', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-3.json`);
});

it('returns false', () => {
expect(apiKeyIsActive()).toBe(false);
});
});
});

describe('hasApiKey', () => {
describe('if api key config is present', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`);
});

it('returns true if api key is present', () => {
expect(hasApiKey()).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
globalSandboxModeEnabled,
showGlobalSandboxModeWarning,
} from '../../../extensions/amplify-helpers/show-global-sandbox-mode-warning';
import { $TSContext } from '../../../../../amplify-cli-core/lib';
import fs from 'fs';
import chalk from 'chalk';

let ctx, amplifyMeta;

jest.mock('amplify-cli-core', () => ({
stateManager: {
getMeta: jest.fn(() => JSON.parse(amplifyMeta.toString())),
},
}));

describe('global sandbox mode warning', () => {
beforeEach(() => {
const envName = 'dev';
ctx = {
amplify: {
getEnvInfo() {
return { envName };
},
},
print: {
info() {
// noop
},
},
} as unknown as $TSContext;
});

describe('globalSandboxModeEnabled', () => {
describe('enabled', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`);
});

it('returns true', async () => {
expect(globalSandboxModeEnabled(ctx)).toBe(true);
});
});

describe('not specified', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-2.json`);
});

it('returns false', async () => {
expect(globalSandboxModeEnabled(ctx)).toBe(false);
});
});
});

describe('showGlobalSandboxModeWarning', () => {
describe('sandbox mode enabled', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta.json`);
});

it('prints warning message', async () => {
jest.spyOn(ctx.print, 'info');

await showGlobalSandboxModeWarning(ctx);

expect(ctx.print.info).toBeCalledWith(`
⚠️ WARNING: ${chalk.green('"type AMPLIFY_GLOBAL @allow_public_data_access_with_api_key"')} in your GraphQL schema
allows public create, read, update, and delete access to all models via API Key. This
should only be used for testing purposes. API Key expiration date is: 8/20/2021

To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql-transformer/auth
`);
});
});

describe('sandbox mode not specified', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-2.json`);
});

it('does not print warning', async () => {
jest.spyOn(ctx.print, 'info');

await showGlobalSandboxModeWarning(ctx);

expect(ctx.print.info).toBeCalledTimes(0);
});
});

describe('no api key config', () => {
beforeAll(() => {
amplifyMeta = fs.readFileSync(`${__dirname}/testData/mockLocalCloud/amplify-meta-3.json`);
});

it('does not print warning', async () => {
jest.spyOn(ctx.print, 'info');

await showGlobalSandboxModeWarning(ctx);

expect(ctx.print.info).toBeCalledTimes(0);
});
});
});
});
Loading