packages/aws-cdk-lib/aws-events/lib/event-bus.ts (267 lines of code) (raw):

import { Construct } from 'constructs'; import { Archive, BaseArchiveProps } from './archive'; import { CfnEventBus, CfnEventBusPolicy } from './events.generated'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as sqs from '../../aws-sqs'; import { Annotations, ArnFormat, FeatureFlags, IResource, Lazy, Names, Resource, Stack, Token, UnscopedValidationError, ValidationError } from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import * as cxapi from '../../cx-api'; /** * Interface which all EventBus based classes MUST implement */ export interface IEventBus extends IResource { /** * The physical ID of this event bus resource * * @attribute * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-name */ readonly eventBusName: string; /** * The ARN of this event bus resource * * @attribute * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#Arn-fn::getatt */ readonly eventBusArn: string; /** * The JSON policy of this event bus resource * * @attribute * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#Policy-fn::getatt */ readonly eventBusPolicy: string; /** * The partner event source to associate with this event bus resource * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-eventsourcename */ readonly eventSourceName?: string; /** * Create an EventBridge archive to send events to. * When you create an archive, incoming events might not immediately start being sent to the archive. * Allow a short period of time for changes to take effect. * * @param props Properties of the archive */ archive(id: string, props: BaseArchiveProps): Archive; /** * Grants an IAM Principal to send custom events to the eventBus * so that they can be matched to rules. * * @param grantee The principal (no-op if undefined) * @param sid The Statement ID used if we need to add a trust policy on the event bus. * */ grantPutEventsTo(grantee: iam.IGrantable, sid?: string): iam.Grant; } /** * Properties to define an event bus */ export interface EventBusProps { /** * The name of the event bus you are creating * Note: If 'eventSourceName' is passed in, you cannot set this * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-name * @default - automatically generated name */ readonly eventBusName?: string; /** * The partner event source to associate with this event bus resource * Note: If 'eventBusName' is passed in, you cannot set this * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-eventsourcename * @default - no partner event source */ readonly eventSourceName?: string; /** * Dead-letter queue for the event bus * * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-event-delivery.html#eb-rule-dlq * * @default - no dead-letter queue */ readonly deadLetterQueue?: sqs.IQueue; /** * The event bus description. * * The description can be up to 512 characters long. * * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-description * * @default - no description */ readonly description?: string; /** * The customer managed key that encrypt events on this event bus. * * @default - Use an AWS managed key */ readonly kmsKey?: kms.IKey; } /** * Interface with properties necessary to import a reusable EventBus */ export interface EventBusAttributes { /** * The physical ID of this event bus resource * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-name */ readonly eventBusName: string; /** * The ARN of this event bus resource * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#Arn-fn::getatt */ readonly eventBusArn: string; /** * The JSON policy of this event bus resource * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#Policy-fn::getatt */ readonly eventBusPolicy: string; /** * The partner event source to associate with this event bus resource * * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-eventsourcename * @default - no partner event source */ readonly eventSourceName?: string; } abstract class EventBusBase extends Resource implements IEventBus, iam.IResourceWithPolicy { /** * The physical ID of this event bus resource */ public abstract readonly eventBusName: string; /** * The ARN of the event bus, such as: * arn:aws:events:us-east-2:123456789012:event-bus/aws.partner/PartnerName/acct1/repo1. */ public abstract readonly eventBusArn: string; /** * The policy for the event bus in JSON form. */ public abstract readonly eventBusPolicy: string; /** * The name of the partner event source */ public abstract readonly eventSourceName?: string; public archive(id: string, props: BaseArchiveProps): Archive { return new Archive(this, id, { sourceEventBus: this, description: props.description || `Event Archive for ${this.eventBusName} Event Bus`, eventPattern: props.eventPattern, retention: props.retention, archiveName: props.archiveName, }); } public grantPutEventsTo(grantee: iam.IGrantable, sid?: string): iam.Grant { const actions = ['events:PutEvents']; const resourceArns = [this.eventBusArn]; const options = { grantee, actions: actions, resourceArns: [this.eventBusArn], }; const grantResult = iam.Grant.addToPrincipal(options); if (grantResult.success) { return grantResult; } const requireSid = FeatureFlags.of(this).isEnabled(cxapi.EVENTBUS_POLICY_SID_REQUIRED); if (requireSid) { const statement = new iam.PolicyStatement({ actions: actions, resources: resourceArns, principals: [grantee!.grantPrincipal], sid: sid, }); return iam.Grant.addStatementToResourcePolicy({ ...options, statement, resource: this }); } else { Annotations.of(this).addWarningV2( '@aws-cdk/aws-events:eventBusServicePrincipalGrant', 'Unable to grant PutEvents to service principal: Statement ID is required for EventBus resource policies. Either provide a \'sid\' parameter or enable the \'@aws-cdk/aws-events:requireEventBusPolicySid\' feature flag.', ); return iam.Grant.drop(grantee, ''); } } /** * Adds a statement to the resource policy associated with this event bus. * A resource policy will be automatically created upon the first call to `addToResourcePolicy`. * * Note that this does not work with imported event buss. * * @param statement The policy statement to add */ public abstract addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; } /** * Define an EventBridge EventBus * * @resource AWS::Events::EventBus */ export class EventBus extends EventBusBase { /** * Import an existing event bus resource * @param scope Parent construct * @param id Construct ID * @param eventBusArn ARN of imported event bus */ public static fromEventBusArn(scope: Construct, id: string, eventBusArn: string): IEventBus { const parts = Stack.of(scope).splitArn(eventBusArn, ArnFormat.SLASH_RESOURCE_NAME); return new ImportedEventBus(scope, id, { eventBusArn: eventBusArn, eventBusName: parts.resourceName || '', eventBusPolicy: '', }); } /** * Import an existing event bus resource * @param scope Parent construct * @param id Construct ID * @param eventBusName Name of imported event bus */ public static fromEventBusName(scope: Construct, id: string, eventBusName: string): IEventBus { const eventBusArn = Stack.of(scope).formatArn({ resource: 'event-bus', service: 'events', resourceName: eventBusName, }); return EventBus.fromEventBusAttributes(scope, id, { eventBusName: eventBusName, eventBusArn: eventBusArn, eventBusPolicy: '', }); } /** * Import an existing event bus resource * @param scope Parent construct * @param id Construct ID * @param attrs Imported event bus properties */ public static fromEventBusAttributes(scope: Construct, id: string, attrs: EventBusAttributes): IEventBus { return new ImportedEventBus(scope, id, attrs); } /** * Permits an IAM Principal to send custom events to EventBridge * so that they can be matched to rules. * * @param grantee The principal (no-op if undefined) * @deprecated use grantAllPutEvents instead */ public static grantPutEvents(grantee: iam.IGrantable): iam.Grant { // It's currently not possible to restrict PutEvents to specific resources. // See https://docs.aws.amazon.com/eventbridge/latest/userguide/permissions-reference-eventbridge.html return iam.Grant.addToPrincipal({ grantee, actions: ['events:PutEvents'], resourceArns: ['*'], }); } /** * Permits an IAM Principal to send custom events to EventBridge * so that they can be matched to rules. * * @param grantee The principal (no-op if undefined) */ public static grantAllPutEvents(grantee: iam.IGrantable): iam.Grant { return iam.Grant.addToPrincipal({ grantee, actions: ['events:PutEvents'], resourceArns: ['*'], }); } private static eventBusProps(defaultEventBusName: string, props: EventBusProps = {}) { const { eventBusName, eventSourceName } = props; const eventBusNameRegex = /^[\/\.\-_A-Za-z0-9]{1,256}$/; if (eventBusName !== undefined && eventSourceName !== undefined) { throw new UnscopedValidationError( '\'eventBusName\' and \'eventSourceName\' cannot both be provided', ); } if (eventBusName !== undefined) { if (!Token.isUnresolved(eventBusName)) { if (eventBusName === 'default') { throw new UnscopedValidationError( '\'eventBusName\' must not be \'default\'', ); } else if (eventBusName.indexOf('/') > -1) { throw new UnscopedValidationError( '\'eventBusName\' must not contain \'/\'', ); } else if (!eventBusNameRegex.test(eventBusName)) { throw new UnscopedValidationError( `'eventBusName' must satisfy: ${eventBusNameRegex}`, ); } } return { eventBusName }; } if (eventSourceName !== undefined) { if (!Token.isUnresolved(eventSourceName)) { // Ex: aws.partner/PartnerName/acct1/repo1 const eventSourceNameRegex = /^aws\.partner(\/[\.\-_A-Za-z0-9]+){2,}$/; if (!eventSourceNameRegex.test(eventSourceName)) { throw new UnscopedValidationError( `'eventSourceName' must satisfy: ${eventSourceNameRegex}`, ); } else if (!eventBusNameRegex.test(eventSourceName)) { throw new UnscopedValidationError( `'eventSourceName' must satisfy: ${eventBusNameRegex}`, ); } } return { eventBusName: eventSourceName, eventSourceName }; } return { eventBusName: defaultEventBusName }; } /** * The physical ID of this event bus resource */ public readonly eventBusName: string; /** * The ARN of the event bus, such as: * arn:aws:events:us-east-2:123456789012:event-bus/aws.partner/PartnerName/acct1/repo1. */ public readonly eventBusArn: string; /** * The policy for the event bus in JSON form. */ public readonly eventBusPolicy: string; /** * The name of the partner event source */ public readonly eventSourceName?: string; constructor(scope: Construct, id: string, props?: EventBusProps) { const { eventBusName, eventSourceName } = EventBus.eventBusProps( Lazy.string({ produce: () => Names.uniqueId(this) }), props, ); super(scope, id, { physicalName: eventBusName }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); if (props?.description && !Token.isUnresolved(props.description) && props.description.length > 512) { throw new ValidationError(`description must be less than or equal to 512 characters, got ${props.description.length}`, this); } const eventBus = new CfnEventBus(this, 'Resource', { name: this.physicalName, eventSourceName, deadLetterConfig: props?.deadLetterQueue ? { arn: props.deadLetterQueue.queueArn, } : undefined, description: props?.description, kmsKeyIdentifier: props?.kmsKey?.keyArn, }); this.eventBusArn = this.getResourceArnAttribute(eventBus.attrArn, { service: 'events', resource: 'event-bus', resourceName: eventBus.name, }); /** * Allow EventBridge to use customer managed key * * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-encryption-key-policy.html#eb-encryption-key-policy-bus */ if (props?.kmsKey) { props?.kmsKey.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['kms:Decrypt', 'kms:GenerateDataKey', 'kms:DescribeKey'], principals: [new iam.ServicePrincipal('events.amazonaws.com')], conditions: { StringEquals: { 'aws:SourceAccount': this.stack.account, 'aws:SourceArn': Stack.of(this).formatArn({ service: 'events', resource: 'event-bus', resourceName: eventBusName, }), 'kms:EncryptionContext:aws:events:event-bus:arn': Stack.of(this).formatArn({ service: 'events', resource: 'event-bus', resourceName: eventBusName, }), }, }, })); } this.eventBusName = this.getResourceNameAttribute(eventBus.ref); this.eventBusPolicy = eventBus.attrPolicy; this.eventSourceName = eventBus.eventSourceName; } /** * Adds a statement to the IAM resource policy associated with this event bus. */ @MethodMetadata() public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { // If no sid is provided, generate one based on the event bus id if (statement.sid == null) { throw new ValidationError('Event Bus policy statements must have a sid', this); } // In order to generate new statementIDs for the change in https://github.com/aws/aws-cdk/pull/27340 const statementId = `cdk-${statement.sid}`.slice(0, 64); statement.sid = statementId; const policy = new EventBusPolicy(this, statementId, { eventBus: this, statement: statement.toJSON(), statementId, }); return { statementAdded: true, policyDependable: policy }; } } class ImportedEventBus extends EventBusBase { public readonly eventBusArn: string; public readonly eventBusName: string; public readonly eventBusPolicy: string; public readonly eventSourceName?: string; constructor(scope: Construct, id: string, attrs: EventBusAttributes) { const arnParts = Stack.of(scope).splitArn(attrs.eventBusArn, ArnFormat.SLASH_RESOURCE_NAME); super(scope, id, { account: arnParts.account, region: arnParts.region, }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, attrs); this.eventBusArn = attrs.eventBusArn; this.eventBusName = attrs.eventBusName; this.eventBusPolicy = attrs.eventBusPolicy; this.eventSourceName = attrs.eventSourceName; } @MethodMetadata() public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { // Warn the user Annotations.of(this).addWarningV2( '@aws-cdk/aws-events:eventBusAddToResourcePolicy', `Unable to add necessary permissions to imported target event bus: ${this.eventBusArn}`, ); return { statementAdded: false }; } } /** * Properties to associate Event Buses with a policy */ export interface EventBusPolicyProps { /** * The event bus to which the policy applies */ readonly eventBus: IEventBus; /** * An IAM Policy Statement to apply to the Event Bus */ readonly statement: iam.PolicyStatement; /** * An identifier string for the external account that * you are granting permissions to. */ readonly statementId: string; } /** * The policy for an Event Bus * * Policies define the operations that are allowed on this resource. * * You almost never need to define this construct directly. * * All AWS resources that support resource policies have a method called * `addToResourcePolicy()`, which will automatically create a new resource * policy if one doesn't exist yet, otherwise it will add to the existing * policy. * * Prefer to use `addToResourcePolicy()` instead. */ export class EventBusPolicy extends Resource { constructor(scope: Construct, id: string, props: EventBusPolicyProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); new CfnEventBusPolicy(this, 'Resource', { statementId: props.statementId!, statement: props.statement, eventBusName: props.eventBus.eventBusName, }); } }