diff --git a/packages/@aws-cdk/aws-chatbot/README.md b/packages/@aws-cdk/aws-chatbot/README.md index a1fe64575ebf8..83d6afcef7773 100644 --- a/packages/@aws-cdk/aws-chatbot/README.md +++ b/packages/@aws-cdk/aws-chatbot/README.md @@ -9,8 +9,29 @@ --- +AWS Chatbot is an AWS service that enables DevOps and software development teams to use Slack chat rooms to monitor and respond to operational events in their AWS Cloud. AWS Chatbot processes AWS service notifications from Amazon Simple Notification Service (Amazon SNS), and forwards them to Slack chat rooms so teams can analyze and act on them immediately, regardless of location. + This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. ```ts import * as chatbot from '@aws-cdk/aws-chatbot'; + +const slackChannel = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { + slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', + slackWorkspaceId: 'YOUR_SLACK_WORKSPACE_ID', + slackChannelId: 'YOUR_SLACK_CHANNEL_ID', +}); + +slackChannel.addLambdaInvokeCommandPermissions(); +slackChannel.addNotificationPermissions(); +slackChannel.addSupportCommandPermissions(); +slackChannel.addReadOnlyCommandPermissions(); + +slackChannel.addToPrincipalPolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObject', + ], + resources: ['arn:aws:s3:::abc/xyz/123.txt'], +})); ``` diff --git a/packages/@aws-cdk/aws-chatbot/lib/index.ts b/packages/@aws-cdk/aws-chatbot/lib/index.ts index 312fee0796b78..da60385a5ce93 100644 --- a/packages/@aws-cdk/aws-chatbot/lib/index.ts +++ b/packages/@aws-cdk/aws-chatbot/lib/index.ts @@ -1,2 +1,3 @@ // AWS::Chatbot CloudFormation Resources: export * from './chatbot.generated'; +export * from './slack-channel-configuration'; diff --git a/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts b/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts new file mode 100644 index 0000000000000..d7746c6b8d679 --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/lib/slack-channel-configuration.ts @@ -0,0 +1,215 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import { CfnSlackChannelConfiguration } from './chatbot.generated'; + +/** + * Properties for a new Slack channel configuration + */ +export interface SlackChannelConfigurationProps { + + /** + * The name of Slack channel configuration + */ + readonly slackChannelConfigurationName: string; + + /** + * The permission role of Slack channel configuration + * + * @default - A role will be created. + */ + readonly role?: iam.IRole; + + /** + * The ID of the Slack workspace authorized with AWS Chatbot. + * + * To get the workspace ID, you must perform the initial authorization flow with Slack in the AWS Chatbot console. + * Then you can copy and paste the workspace ID from the console. + * For more details, see steps 1-4 in Setting Up AWS Chatbot with Slack in the AWS Chatbot User Guide. + * @see https://docs.aws.amazon.com/chatbot/latest/adminguide/setting-up.html#Setup_intro + */ + readonly slackWorkspaceId: string; + + /** + * The ID of the Slack channel. + * + * To get the ID, open Slack, right click on the channel name in the left pane, then choose Copy Link. + * The channel ID is the 9-character string at the end of the URL. For example, ABCBBLZZZ. + */ + readonly slackChannelId: string; + + /** + * The SNS topics that deliver notifications to AWS Chatbot. + * + * @default None + */ + readonly notificationTopics?: sns.ITopic[]; + + /** + * Specifies the logging level for this configuration. + * This property affects the log entries pushed to Amazon CloudWatch Logs. + * + * @default LoggingLevel.NONE + */ + readonly loggingLevel?: LoggingLevel; +} + +/** + * Logging levels include ERROR, INFO, or NONE. + */ +export enum LoggingLevel { + /** + * ERROR + */ + ERROR = 'ERROR', + + /** + * INFO + */ + INFO = 'INFO', + + /** + * NONE + */ + NONE = 'NONE', +} + +/** + * Represents a Slack channel configuration + */ +export interface ISlackChannelConfiguration extends cdk.IResource, iam.IGrantable { + + /** + * The ARN of the Slack channel configuration + * In the form of arn:aws:chatbot:{region}:{account}:chat-configuration/slack-channel/{slackChannelName} + * @attribute + */ + readonly slackChannelConfigurationArn: string; + + /** + * The name of Slack channel configuration + * @attribute + */ + readonly slackChannelConfigurationName: string; + + /** + * The permission role of Slack channel configuration + * @attribute + * + * @default - A role will be created. + */ + readonly role?: iam.IRole; + + /** + * Adds a statement to the IAM role. + */ + addToRolePolicy(statement: iam.PolicyStatement): void; +} + +/** + * Either a new or imported Slack channel configuration + */ +abstract class SlackChannelConfigurationBase extends cdk.Resource implements ISlackChannelConfiguration { + abstract readonly slackChannelConfigurationArn: string; + + abstract readonly slackChannelConfigurationName: string; + + abstract readonly grantPrincipal: iam.IPrincipal; + + abstract readonly role?: iam.IRole; + + /** + * Adds extra permission to iam-role of Slack channel configuration + * @param statement + */ + public addToRolePolicy(statement: iam.PolicyStatement): void { + if (!this.role) { + return; + } + + this.role.addToPrincipalPolicy(statement); + } +} + +/** + * A new Slack channel configuration + */ +export class SlackChannelConfiguration extends SlackChannelConfigurationBase { + + /** + * Import an existing Slack channel configuration provided an ARN + * @param scope The parent creating construct + * @param id The construct's name + * @param slackChannelConfigurationArn configuration ARN (i.e. arn:aws:chatbot::1234567890:chat-configuration/slack-channel/my-slack) + * + * @returns a reference to the existing Slack channel configuration + */ + public static fromSlackChannelConfigurationArn(scope: cdk.Construct, id: string, slackChannelConfigurationArn: string): ISlackChannelConfiguration { + const re = /^slack-channel\//; + const resourceName = cdk.Stack.of(scope).parseArn(slackChannelConfigurationArn).resourceName as string; + + if (!re.test(resourceName)) { + throw new Error('The ARN of a Slack integration must be in the form: arn:aws:chatbot:{region}:{account}:chat-configuration/slack-channel/{slackChannelName}'); + } + + class Import extends SlackChannelConfigurationBase { + + /** + * @attribute + */ + readonly slackChannelConfigurationArn = slackChannelConfigurationArn; + readonly role?: iam.IRole = undefined; + readonly grantPrincipal: iam.IPrincipal; + + /** + * Returns a name of Slack channel configuration + * + * NOTE: + * For example: arn:aws:chatbot::1234567890:chat-configuration/slack-channel/my-slack + * The ArnComponents API will return `slack-channel/my-slack` + * It need to handle that to gets a correct name.`my-slack` + */ + readonly slackChannelConfigurationName = resourceName.substring('slack-channel/'.length); + + constructor(s: cdk.Construct, i: string) { + super(s, i); + this.grantPrincipal = new iam.UnknownPrincipal({ resource: this }); + } + } + + return new Import(scope, id); + } + + readonly slackChannelConfigurationArn: string; + + readonly slackChannelConfigurationName: string; + + readonly role?: iam.IRole; + + readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: cdk.Construct, id: string, props: SlackChannelConfigurationProps) { + super(scope, id, { + physicalName: props.slackChannelConfigurationName, + }); + + this.role = props.role || new iam.Role(this, 'ConfigurationRole', { + assumedBy: new iam.ServicePrincipal('chatbot.amazonaws.com'), + }); + + this.grantPrincipal = this.role; + + const configuration = new CfnSlackChannelConfiguration(this, 'Resource', { + configurationName: props.slackChannelConfigurationName, + iamRoleArn: this.role.roleArn, + slackWorkspaceId: props.slackWorkspaceId, + slackChannelId: props.slackChannelId, + snsTopicArns: props.notificationTopics?.map(topic => topic.topicArn), + loggingLevel: props.loggingLevel?.toString(), + }); + + this.slackChannelConfigurationArn = configuration.ref; + this.slackChannelConfigurationName = props.slackChannelConfigurationName; + } +} + diff --git a/packages/@aws-cdk/aws-chatbot/package.json b/packages/@aws-cdk/aws-chatbot/package.json index 490d51ea95424..2a2acb7013526 100644 --- a/packages/@aws-cdk/aws-chatbot/package.json +++ b/packages/@aws-cdk/aws-chatbot/package.json @@ -68,14 +68,21 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.4" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.4" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-chatbot/test/chatbot.test.ts b/packages/@aws-cdk/aws-chatbot/test/chatbot.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-chatbot/test/chatbot.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-chatbot/test/integ.chatbot.expected.json b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot.expected.json new file mode 100644 index 0000000000000..1cf6a7afbdd1f --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot.expected.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "MySlackChannelConfigurationRole1D3F23AE": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "chatbot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MySlackChannelConfigurationRoleDefaultPolicyE4C1FA62": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": "arn:aws:s3:::abc/xyz/123.txt" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MySlackChannelConfigurationRoleDefaultPolicyE4C1FA62", + "Roles": [ + { + "Ref": "MySlackChannelConfigurationRole1D3F23AE" + } + ] + } + }, + "MySlackChannelA8E0B56C": { + "Type": "AWS::Chatbot::SlackChannelConfiguration", + "Properties": { + "ConfigurationName": "test-channel", + "IamRoleArn": { + "Fn::GetAtt": [ + "MySlackChannelConfigurationRole1D3F23AE", + "Arn" + ] + }, + "SlackChannelId": "C0187JABUE9", + "SlackWorkspaceId": "T49239U4W", + "LoggingLevel": "NONE" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-chatbot/test/integ.chatbot.ts b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot.ts new file mode 100644 index 0000000000000..c005dfa873abe --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/test/integ.chatbot.ts @@ -0,0 +1,31 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as chatbot from '../lib'; + +class ChatbotInteg extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const slackChannel = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel', { + slackChannelConfigurationName: 'test-channel', + slackWorkspaceId: 'T49239U4W', // modify to your slack workspace id + slackChannelId: 'C0187JABUE9', // modify to your slack channel id + loggingLevel: chatbot.LoggingLevel.NONE, + }); + + slackChannel.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObject', + ], + resources: ['arn:aws:s3:::abc/xyz/123.txt'], + })); + } +} + +const app = new cdk.App(); + +new ChatbotInteg(app, 'ChatbotInteg'); + +app.synth(); + diff --git a/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts b/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts new file mode 100644 index 0000000000000..3cf1189d9fee2 --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/test/slack-channel-configuration.test.ts @@ -0,0 +1,168 @@ +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as chatbot from '../lib'; + +describe('SlackChannelConfiguration', () => { + let stack: cdk.Stack; + + beforeEach(() => { + stack = new cdk.Stack(); + }); + + test('created with minimal properties creates a new IAM Role', () => { + new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'Test', + }); + + expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + ConfigurationName: 'Test', + IamRoleArn: { + 'Fn::GetAtt': [ + 'MySlackChannelConfigurationRole1D3F23AE', + 'Arn', + ], + }, + SlackChannelId: 'DEF456', + SlackWorkspaceId: 'ABC123', + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'chatbot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('created and pass loggingLevel parameter [LoggingLevel.ERROR], it should be set [ERROR] logging level in Cloudformation', () => { + new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'Test', + loggingLevel: chatbot.LoggingLevel.ERROR, + }); + + expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + ConfigurationName: 'Test', + IamRoleArn: { + 'Fn::GetAtt': [ + 'MySlackChannelConfigurationRole1D3F23AE', + 'Arn', + ], + }, + SlackChannelId: 'DEF456', + SlackWorkspaceId: 'ABC123', + LoggingLevel: 'ERROR', + }); + }); + + test('created with new sns topic', () => { + const topic = new sns.Topic(stack, 'MyTopic'); + + new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'Test', + notificationTopics: [topic], + }); + + expect(stack).toHaveResourceLike('AWS::Chatbot::SlackChannelConfiguration', { + ConfigurationName: 'Test', + IamRoleArn: { + 'Fn::GetAtt': [ + 'MySlackChannelConfigurationRole1D3F23AE', + 'Arn', + ], + }, + SlackChannelId: 'DEF456', + SlackWorkspaceId: 'ABC123', + SnsTopicArns: [ + { + Ref: 'MyTopic86869434', + }, + ], + }); + }); + + test('created with existing role', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam:::role/test-role'); + + new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'Test', + role: role, + }); + + expect(stack).toCountResources('AWS::IAM::Role', 0); + }); + + test('created with new role and extra iam policies', () => { + const slackChannel = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackWorkspaceId: 'ABC123', + slackChannelId: 'DEF456', + slackChannelConfigurationName: 'Test', + }); + + slackChannel.addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObject', + ], + resources: ['arn:aws:s3:::abc/xyz/123.txt'], + })); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetObject', + Effect: 'Allow', + Resource: 'arn:aws:s3:::abc/xyz/123.txt', + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('added a iam policy to a from slack channel configuration ARN will nothing to do', () => { + const imported = chatbot.SlackChannelConfiguration.fromSlackChannelConfigurationArn(stack, 'MySlackChannel', 'arn:aws:chatbot::1234567890:chat-configuration/slack-channel/my-slack'); + + (imported as chatbot.SlackChannelConfiguration).addToRolePolicy(new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObject', + ], + resources: ['arn:aws:s3:::abc/xyz/123.txt'], + })); + + expect(stack).toCountResources('AWS::IAM::Role', 0); + expect(stack).toCountResources('AWS::IAM::Policy', 0); + }); + + test('should throw error if ARN invalid', () => { + expect(() => chatbot.SlackChannelConfiguration.fromSlackChannelConfigurationArn(stack, 'MySlackChannel', 'arn:aws:chatbot::1234567890:chat-configuration/my-slack')).toThrow( + /The ARN of a Slack integration must be in the form: arn:aws:chatbot:{region}:{account}:chat-configuration\/slack-channel\/{slackChannelName}/, + ); + }); + + test('from slack channel configuration ARN', () => { + const imported = chatbot.SlackChannelConfiguration.fromSlackChannelConfigurationArn(stack, 'MySlackChannel', 'arn:aws:chatbot::1234567890:chat-configuration/slack-channel/my-slack'); + + expect(imported.slackChannelConfigurationName).toEqual('my-slack'); + expect(imported.slackChannelConfigurationArn).toEqual('arn:aws:chatbot::1234567890:chat-configuration/slack-channel/my-slack'); + }); +}); \ No newline at end of file