From e89688ec1dd7a3b072d23287cddcb73bccc16fd4 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Fri, 4 Feb 2022 06:15:49 +0900 Subject: [PATCH] feat(iotevents): add grant method to Input class (#18617) This PR add `grant` method to `Input` class. Next of this PR, I aim to create PR that add IoT Event Action to IoT Core Rule. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-iotevents/README.md | 13 +++ .../aws-iotevents/lib/detector-model.ts | 4 +- .../@aws-cdk/aws-iotevents/lib/expression.ts | 10 +-- packages/@aws-cdk/aws-iotevents/lib/input.ts | 77 ++++++++++++++--- packages/@aws-cdk/aws-iotevents/lib/state.ts | 10 +-- .../@aws-cdk/aws-iotevents/test/input.test.ts | 82 +++++++++++++++++-- 6 files changed, 165 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 760b6098d9d41..864833049b402 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -70,3 +70,16 @@ new iotevents.DetectorModel(this, 'MyDetectorModel', { initialState: onlineState, }); ``` + +To grant permissions to put messages in the input, +you can use the `grantWrite()` method: + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const grantable: iam.IGrantable; +const input = iotevents.Input.fromInputName(this, 'MyInput', 'my_input'); + +input.grantWrite(grantable); +``` diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts index 5ef50fd871d75..a35b1efc30d23 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -5,7 +5,7 @@ import { CfnDetectorModel } from './iotevents.generated'; import { State } from './state'; /** - * Represents an AWS IoT Events detector model + * Represents an AWS IoT Events detector model. */ export interface IDetectorModel extends IResource { /** @@ -33,7 +33,7 @@ export enum EventEvaluation { } /** - * Properties for defining an AWS IoT Events detector model + * Properties for defining an AWS IoT Events detector model. */ export interface DetectorModelProps { /** diff --git a/packages/@aws-cdk/aws-iotevents/lib/expression.ts b/packages/@aws-cdk/aws-iotevents/lib/expression.ts index 27fdf069c1b9f..fd686e9761802 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/expression.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/expression.ts @@ -1,12 +1,12 @@ import { IInput } from './input'; /** - * Expression for events in Detector Model state + * Expression for events in Detector Model state. * @see https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-expressions.html */ export abstract class Expression { /** - * Create a expression from the given string + * Create a expression from the given string. */ public static fromString(value: string): Expression { return new StringExpression(value); @@ -28,14 +28,14 @@ export abstract class Expression { } /** - * Create a expression for the Equal operator + * Create a expression for the Equal operator. */ public static eq(left: Expression, right: Expression): Expression { return new BinaryOperationExpression(left, '==', right); } /** - * Create a expression for the AND operator + * Create a expression for the AND operator. */ public static and(left: Expression, right: Expression): Expression { return new BinaryOperationExpression(left, '&&', right); @@ -45,7 +45,7 @@ export abstract class Expression { } /** - * this is called to evaluate the expression + * This is called to evaluate the expression. */ public abstract evaluate(): string; } diff --git a/packages/@aws-cdk/aws-iotevents/lib/input.ts b/packages/@aws-cdk/aws-iotevents/lib/input.ts index e4bba5684b7a4..b656af2d4dff6 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/input.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/input.ts @@ -1,24 +1,66 @@ -import { Resource, IResource } from '@aws-cdk/core'; +import * as iam from '@aws-cdk/aws-iam'; +import { Resource, IResource, Aws } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnInput } from './iotevents.generated'; /** - * Represents an AWS IoT Events input + * Represents an AWS IoT Events input. */ export interface IInput extends IResource { /** - * The name of the input + * The name of the input. + * * @attribute */ readonly inputName: string; + + /** + * The ARN of the input. + * + * @attribute + */ + readonly inputArn: string; + + /** + * Grant write permissions on this input and its contents to an IAM principal (Role/Group/User). + * + * @param grantee the principal + */ + grantWrite(grantee: iam.IGrantable): iam.Grant + + /** + * Grant the indicated permissions on this input to the given IAM principal (Role/Group/User). + * + * @param grantee the principal + * @param actions the set of actions to allow (i.e. "iotevents:BatchPutMessage") + */ + grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant +} + +abstract class InputBase extends Resource implements IInput { + public abstract readonly inputName: string; + + public abstract readonly inputArn: string; + + public grantWrite(grantee: iam.IGrantable) { + return this.grant(grantee, 'iotevents:BatchPutMessage'); + } + + public grant(grantee: iam.IGrantable, ...actions: string[]) { + return iam.Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [this.inputArn], + }); + } } /** - * Properties for defining an AWS IoT Events input + * Properties for defining an AWS IoT Events input. */ export interface InputProps { /** - * The name of the input + * The name of the input. * * @default - CloudFormation will generate a unique name of the input */ @@ -37,19 +79,25 @@ export interface InputProps { /** * Defines an AWS IoT Events input in this stack. */ -export class Input extends Resource implements IInput { +export class Input extends InputBase { /** - * Import an existing input + * Import an existing input. */ public static fromInputName(scope: Construct, id: string, inputName: string): IInput { - class Import extends Resource implements IInput { + return new class Import extends InputBase { public readonly inputName = inputName; - } - return new Import(scope, id); + public readonly inputArn = this.stack.formatArn({ + service: 'iotevents', + resource: 'input', + resourceName: inputName, + }); + }(scope, id); } public readonly inputName: string; + public readonly inputArn: string; + constructor(scope: Construct, id: string, props: InputProps) { super(scope, id, { physicalName: props.inputName, @@ -67,5 +115,14 @@ export class Input extends Resource implements IInput { }); this.inputName = this.getResourceNameAttribute(resource.ref); + this.inputArn = this.getResourceArnAttribute(arnForInput(resource.ref), { + service: 'iotevents', + resource: 'input', + resourceName: this.physicalName, + }); } } + +function arnForInput(inputName: string): string { + return `arn:${Aws.PARTITION}:iotevents:${Aws.REGION}:${Aws.ACCOUNT_ID}:input/${inputName}`; +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index e16d911d60004..129d3395776ad 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -2,7 +2,7 @@ import { Event } from './event'; import { CfnDetectorModel } from './iotevents.generated'; /** - * Properties for defining a state of a detector + * Properties for defining a state of a detector. */ export interface StateProps { /** @@ -20,11 +20,11 @@ export interface StateProps { } /** - * Defines a state of a detector + * Defines a state of a detector. */ export class State { /** - * The name of the state + * The name of the state. */ public readonly stateName: string; @@ -33,7 +33,7 @@ export class State { } /** - * Return the state property JSON + * Return the state property JSON. * * @internal */ @@ -46,7 +46,7 @@ export class State { } /** - * returns true if this state has at least one condition via events + * Returns true if this state has at least one condition via events. * * @internal */ diff --git a/packages/@aws-cdk/aws-iotevents/test/input.test.ts b/packages/@aws-cdk/aws-iotevents/test/input.test.ts index 11b457bb0cf1b..8907489af928e 100644 --- a/packages/@aws-cdk/aws-iotevents/test/input.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/input.test.ts @@ -1,10 +1,14 @@ import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as iotevents from '../lib'; -test('Default property', () => { - const stack = new cdk.Stack(); +let stack: cdk.Stack; +beforeEach(() => { + stack = new cdk.Stack(); +}); +test('Default property', () => { // WHEN new iotevents.Input(stack, 'MyInput', { attributeJsonPaths: ['payload.temperature'], @@ -19,7 +23,6 @@ test('Default property', () => { }); test('can get input name', () => { - const stack = new cdk.Stack(); // GIVEN const input = new iotevents.Input(stack, 'MyInput', { attributeJsonPaths: ['payload.temperature'], @@ -39,9 +42,38 @@ test('can get input name', () => { }); }); -test('can set physical name', () => { - const stack = new cdk.Stack(); +test('can get input ARN', () => { + // GIVEN + const input = new iotevents.Input(stack, 'MyInput', { + attributeJsonPaths: ['payload.temperature'], + }); + // WHEN + new cdk.CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + InputArn: input.inputArn, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Test::Resource', { + InputArn: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iotevents:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':input/', + { Ref: 'MyInput08947B23' }, + ]], + }, + }); +}); + +test('can set physical name', () => { // WHEN new iotevents.Input(stack, 'MyInput', { inputName: 'test_input', @@ -55,8 +87,6 @@ test('can set physical name', () => { }); test('can import a Input by inputName', () => { - const stack = new cdk.Stack(); - // WHEN const inputName = 'test-input-name'; const topicRule = iotevents.Input.fromInputName(stack, 'InputFromInputName', inputName); @@ -68,11 +98,45 @@ test('can import a Input by inputName', () => { }); test('cannot be created with an empty array of attributeJsonPaths', () => { - const stack = new cdk.Stack(); - expect(() => { new iotevents.Input(stack, 'MyInput', { attributeJsonPaths: [], }); }).toThrow('attributeJsonPaths property cannot be empty'); }); + +test('can grant the permission to put message', () => { + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::account-id:role/role-name'); + const input = new iotevents.Input(stack, 'MyInput', { + attributeJsonPaths: ['payload.temperature'], + }); + + // WHEN + input.grantWrite(role); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'iotevents:BatchPutMessage', + Effect: 'Allow', + Resource: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iotevents:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':input/', + { Ref: 'MyInput08947B23' }, + ]], + }, + }, + ], + }, + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['role-name'], + }); +});