From 18191aa98af7d018ea7d8d640239f9bef975e3c6 Mon Sep 17 00:00:00 2001 From: Akash Askoolum Date: Tue, 22 Jun 2021 12:15:43 +0100 Subject: [PATCH] feat: Add policy to allow writing to Anghammarad Create a policy that allows `sns:Publish` to the Anghammarad SNS topic. The Anghammarad SNS topic is used across multiple stacks, so we can promote it to an account wide ("account/services") parameter store location. We then wrap this account wide parameter into the `AnghammaradTopicParameter` singleton, which the policy will add when necessary. --- docs/005-default-parameter-store-locations.md | 15 ++--- src/constructs/core/parameters/anghammarad.ts | 35 +++++++++++ .../iam/policies/anghammarad.test.ts | 60 +++++++++++++++++++ src/constructs/iam/policies/anghammarad.ts | 32 ++++++++++ 4 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 src/constructs/core/parameters/anghammarad.ts create mode 100644 src/constructs/iam/policies/anghammarad.test.ts create mode 100644 src/constructs/iam/policies/anghammarad.ts diff --git a/docs/005-default-parameter-store-locations.md b/docs/005-default-parameter-store-locations.md index c1431050e9..ff8cb4ebba 100644 --- a/docs/005-default-parameter-store-locations.md +++ b/docs/005-default-parameter-store-locations.md @@ -3,13 +3,14 @@ A number of constructs from the library define parameters to get configuration from Parameter Store. Each of these parameters configures a default path. The below table lists those paths, the parameter that sets them, the expected value type and which construct the parameter is defined within. -| Path | Parameter | Type | Construct | -| ------------------------------------- | ---------------------- | ---------------------------- | -------------------------- | -| /account/vpc/primary/id | VpcId | AWS::EC2::VPC::Id | GuVpc.fromIdParameter | -| /account/vpc/primary/subnets/public | PublicSubnets | List\ | GuVpc.subnetsFromParameter | -| /account/vpc/primary/subnets/private | PrivateSubnets | List\ | GuVpc.subnetsFromParameter | -| /account/services/artifact.bucket | DistributionBucketName | String | GuGetDistributablePolicy | -| /account/services/logging.stream.name | LoggingStreamName | String | GuLogShippingPolicy | +| Path | Parameter | Type | Construct | +| --------------------------------------- | ---------------------- | ---------------------------- | -------------------------- | +| /account/vpc/primary/id | VpcId | AWS::EC2::VPC::Id | GuVpc.fromIdParameter | +| /account/vpc/primary/subnets/public | PublicSubnets | List\ | GuVpc.subnetsFromParameter | +| /account/vpc/primary/subnets/private | PrivateSubnets | List\ | GuVpc.subnetsFromParameter | +| /account/services/artifact.bucket | DistributionBucketName | String | GuGetDistributablePolicy | +| /account/services/logging.stream.name | LoggingStreamName | String | GuLogShippingPolicy | +| /account/services/anghammarad.topic.arn | AnghammaradSnsArn | String | AnghammaradSenderPolicy | ## Pattern-specific default Parameter Store locations diff --git a/src/constructs/core/parameters/anghammarad.ts b/src/constructs/core/parameters/anghammarad.ts new file mode 100644 index 0000000000..6a2b07fc85 --- /dev/null +++ b/src/constructs/core/parameters/anghammarad.ts @@ -0,0 +1,35 @@ +import { isSingletonPresentInStack } from "../../../utils/test"; +import type { GuStack } from "../stack"; +import { GuStringParameter } from "./base"; + +/** + * Creates a CloudFormation parameter to a SSM Parameter Store item that holds the ARN of the Anghammarad SNS topic. + * This parameter is implemented as a singleton, meaning only one can ever be added to a stack and will be reused if necessary. + * + * @see https://github.com/guardian/anghammarad + */ +export class AnghammaradTopicParameter extends GuStringParameter { + private static instance: AnghammaradTopicParameter | undefined; + + private constructor(scope: GuStack) { + super(scope, "AnghammaradSnsArn", { + fromSSM: true, + default: "/account/services/anghammarad.topic.arn", + description: "SSM parameter containing the ARN of the Anghammarad SNS topic", + }); + } + + /** + * Returns a pre-existing parameter in the stack. + * If no parameter exists, creates a new parameter. + * + * @param stack the stack to operate on + */ + public static getInstance(stack: GuStack): AnghammaradTopicParameter { + if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) { + this.instance = new AnghammaradTopicParameter(stack); + } + + return this.instance; + } +} diff --git a/src/constructs/iam/policies/anghammarad.test.ts b/src/constructs/iam/policies/anghammarad.test.ts new file mode 100644 index 0000000000..0b1ca8d0e6 --- /dev/null +++ b/src/constructs/iam/policies/anghammarad.test.ts @@ -0,0 +1,60 @@ +import "@aws-cdk/assert/jest"; +import { SynthUtils } from "@aws-cdk/assert/lib/synth-utils"; +import type { SynthedStack } from "../../../utils/test"; +import { attachPolicyToTestRole, simpleGuStackForTesting } from "../../../utils/test"; +import type { GuStack } from "../../core"; +import { AnghammaradTopicParameter } from "../../core/parameters/anghammarad"; +import { AnghammaradSenderPolicy } from "./anghammarad"; + +describe("AnghammaradSenderPolicy", () => { + const getParams = (stack: GuStack) => { + const json = SynthUtils.toCloudFormation(stack) as SynthedStack; + return Object.keys(json.Parameters); + }; + + it("should add a parameter to the stack if it is not already defined", () => { + const stack = simpleGuStackForTesting(); + + // an empty stack should only have `Stage` which GuStack adds + expect(getParams(stack)).toEqual(["Stage"]); + + // add the policy + attachPolicyToTestRole(stack, AnghammaradSenderPolicy.getInstance(stack)); + expect(getParams(stack)).toEqual(["Stage", "AnghammaradSnsArn"]); + }); + + it("should not add a parameter to the stack if it already exists", () => { + const stack = simpleGuStackForTesting(); + + // an empty stack should only have `Stage` which GuStack adds + expect(getParams(stack)).toEqual(["Stage"]); + + // explicitly add an AnghammaradTopicParameter + AnghammaradTopicParameter.getInstance(stack); + expect(getParams(stack)).toEqual(["Stage", "AnghammaradSnsArn"]); + + // add the policy + attachPolicyToTestRole(stack, AnghammaradSenderPolicy.getInstance(stack)); + expect(getParams(stack)).toEqual(["Stage", "AnghammaradSnsArn"]); + }); + + it("should define a policy that would allow writing to SNS", () => { + const stack = simpleGuStackForTesting(); + attachPolicyToTestRole(stack, AnghammaradSenderPolicy.getInstance(stack)); + + expect(stack).toHaveResource("AWS::IAM::Policy", { + PolicyDocument: { + Version: "2012-10-17", + Statement: [ + { + Action: "sns:Publish", + Effect: "Allow", + Resource: { + Ref: "AnghammaradSnsArn", + }, + }, + ], + }, + }); + }); +}); diff --git a/src/constructs/iam/policies/anghammarad.ts b/src/constructs/iam/policies/anghammarad.ts new file mode 100644 index 0000000000..57b98092b9 --- /dev/null +++ b/src/constructs/iam/policies/anghammarad.ts @@ -0,0 +1,32 @@ +import { isSingletonPresentInStack } from "../../../utils/test"; +import type { GuStack } from "../../core"; +import { AnghammaradTopicParameter } from "../../core/parameters/anghammarad"; +import { GuAllowPolicy } from "./base-policy"; + +/** + * Creates an `AWS::IAM::Policy` to grant `sns:Publish` permission to the Anghammarad topic. + * An `AnghammaradSnsArn` parameter will be automatically added to the stack when needed. + * + * @see AnghammaradTopicParameter + * @see https://github.com/guardian/anghammarad + */ +export class AnghammaradSenderPolicy extends GuAllowPolicy { + private static instance: AnghammaradSenderPolicy | undefined; + + private constructor(scope: GuStack) { + const anghammaradTopicParameter = AnghammaradTopicParameter.getInstance(scope); + + super(scope, "GuSESSenderPolicy", { + actions: ["sns:Publish"], + resources: [anghammaradTopicParameter.valueAsString], + }); + } + + public static getInstance(stack: GuStack): AnghammaradSenderPolicy { + if (!this.instance || !isSingletonPresentInStack(stack, this.instance)) { + this.instance = new AnghammaradSenderPolicy(stack); + } + + return this.instance; + } +}