Skip to content

Commit

Permalink
feat: Add GuFastlyKinesisLogRoleExperimental role (#1750)
Browse files Browse the repository at this point in the history
  • Loading branch information
marsavar authored Mar 31, 2023
1 parent 50e7a05 commit b018b32
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 4 deletions.
7 changes: 7 additions & 0 deletions src/constants/fastly-aws-account-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Fastly's AWS account ID.
* This is needed by IAM roles assumed by Fastly in order to write into
* an S3 bucket or a Kinesis stream.
* See https://docs.fastly.com/en/guides/creating-an-aws-iam-role-for-fastly-logging
*/
export const FASTLY_AWS_ACCOUNT_ID = "717331877981";
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./regex-pattern";
export * from "./ssm-parameter-paths";
export * from "./metadata-keys";
export * from "./tracking-tag";
export * from "./fastly-aws-account-id";
5 changes: 1 addition & 4 deletions src/constructs/iam/fastly-logs-iam.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccountPrincipal } from "aws-cdk-lib/aws-iam";
import { FASTLY_AWS_ACCOUNT_ID } from "../../constants/fastly-aws-account-id";
import type { GuStack } from "../core";
import { GuFastlyCustomerIdParameter } from "../core";
import { GuPutS3ObjectsPolicy } from "./policies";
Expand All @@ -17,10 +18,6 @@ export interface GuFastlyLogsIamRoleProps {
path?: string;
}

// Fastly's AWS account ID is used as an external ID when creating the IAM role
// See https://docs.fastly.com/en/guides/creating-an-aws-iam-role-for-fastly-logging
const FASTLY_AWS_ACCOUNT_ID = "717331877981";

/**
* Construct which creates the required IAM resources to support Fastly logging to an S3 bucket.
* Importantly, it does not create a permanent IAM user, which was once a requirement.
Expand Down
1 change: 1 addition & 0 deletions src/experimental/constructs/iam/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./roles";
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The GuFastlyKinesisLogRole construct correctly wires up the policy 1`] = `
{
"Conditions": {
"AwsCdkKinesisEncryptedStreamsUnsupportedRegions": {
"Fn::Or": [
{
"Fn::Equals": [
{
"Ref": "AWS::Region",
},
"cn-north-1",
],
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region",
},
"cn-northwest-1",
],
},
],
},
},
"Metadata": {
"gu:cdk:constructs": [
"GuStack",
"GuKinesisStream",
"GuFastlyCustomerIdParameter",
"GuFastlyKinesisLogRoleExperimental",
"GuKinesisPutRecordsPolicyExperimental",
],
"gu:cdk:version": "TEST",
},
"Parameters": {
"FastlyCustomerId": {
"Default": "/account/external/fastly/customer.id",
"Description": "SSM parameter containing the Fastly Customer ID. Can be obtained from https://manage.fastly.com/account/company by an admin",
"Type": "AWS::SSM::Parameter::Value<String>",
},
},
"Resources": {
"GuKinesisPutRecordsPolicy28D5CD2D": {
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"kinesis:PutRecords",
"kinesis:ListShards",
],
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"testStream8BCA7523",
"Arn",
],
},
},
],
"Version": "2012-10-17",
},
"PolicyName": "GuKinesisPutRecordsPolicy28D5CD2D",
"Roles": [
{
"Ref": "testKinesisLogRole5E3B33EE",
},
],
},
"Type": "AWS::IAM::Policy",
},
"testKinesisLogRole5E3B33EE": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": {
"Ref": "FastlyCustomerId",
},
},
},
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition",
},
":iam::717331877981:root",
],
],
},
},
},
],
"Version": "2012-10-17",
},
"RoleName": "writeToKinesisRoleTest",
"Tags": [
{
"Key": "gu:cdk:version",
"Value": "TEST",
},
{
"Key": "gu:repo",
"Value": "guardian/cdk",
},
{
"Key": "Stack",
"Value": "test-stack",
},
{
"Key": "Stage",
"Value": "TEST",
},
],
},
"Type": "AWS::IAM::Role",
},
"testStream8BCA7523": {
"Properties": {
"RetentionPeriodHours": 24,
"ShardCount": 1,
"StreamEncryption": {
"Fn::If": [
"AwsCdkKinesisEncryptedStreamsUnsupportedRegions",
{
"Ref": "AWS::NoValue",
},
{
"EncryptionType": "KMS",
"KeyId": "alias/aws/kinesis",
},
],
},
"StreamModeDetails": {
"StreamMode": "PROVISIONED",
},
"Tags": [
{
"Key": "gu:cdk:version",
"Value": "TEST",
},
{
"Key": "gu:repo",
"Value": "guardian/cdk",
},
{
"Key": "Stack",
"Value": "test-stack",
},
{
"Key": "Stage",
"Value": "TEST",
},
],
},
"Type": "AWS::Kinesis::Stream",
},
},
}
`;
59 changes: 59 additions & 0 deletions src/experimental/constructs/iam/roles/fastly-kinesis-log.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Template } from "aws-cdk-lib/assertions";
import { FASTLY_AWS_ACCOUNT_ID } from "../../../../constants";
import { GuKinesisStream } from "../../../../constructs/kinesis";
import { simpleGuStackForTesting } from "../../../../utils/test";
import { GuFastlyKinesisLogRoleExperimental } from "./fastly-kinesis-log";

describe("The GuFastlyKinesisLogRole construct", () => {
it("correctly wires up the policy", () => {
const stack = simpleGuStackForTesting();
const testStream = new GuKinesisStream(stack, "testStream");
new GuFastlyKinesisLogRoleExperimental(stack, "testKinesisLogRole", {
stream: testStream,
roleName: "writeToKinesisRoleTest",
});
expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
});

it("assumes the correct role", () => {
const stack = simpleGuStackForTesting();
const testStream = new GuKinesisStream(stack, "testStream");
new GuFastlyKinesisLogRoleExperimental(stack, "testKinesisLogRole", {
stream: testStream,
roleName: "writeToKinesisRoleTest",
});

Template.fromStack(stack).hasResourceProperties("AWS::IAM::Role", {
AssumeRolePolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Condition: {
StringEquals: {
"sts:ExternalId": {
Ref: "FastlyCustomerId",
},
},
},
Effect: "Allow",
Principal: {
AWS: {
"Fn::Join": [
"",
[
"arn:",
{
Ref: "AWS::Partition",
},
`:iam::${FASTLY_AWS_ACCOUNT_ID}:root`,
],
],
},
},
},
],
},
});
});
});
46 changes: 46 additions & 0 deletions src/experimental/constructs/iam/roles/fastly-kinesis-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AccountPrincipal } from "aws-cdk-lib/aws-iam";
import { FASTLY_AWS_ACCOUNT_ID } from "../../../../constants/fastly-aws-account-id";
import type { GuStack } from "../../../../constructs/core";
import { GuFastlyCustomerIdParameter } from "../../../../constructs/core";
import { GuRole } from "../../../../constructs/iam";
import type { GuKinesisStream } from "../../../../constructs/kinesis";
import { GuKinesisPutRecordsPolicyExperimental } from "../../policies/kinesis-put-records";

export interface GuFastlyKinesisLogRoleProps {
/**
* The Kinesis stream into which Fastly will put records
*/
stream: GuKinesisStream;
/**
* Name of the IAM role
*/
roleName?: string;
}

/**
* A construct to create an IAM Role for Fastly to assume in order to write to a
* specific Kinesis stream.
*
* In order to use this construct, an SSM parameter named `/account/external/fastly/customer.id`
* needs to exist in the AWS account's parameter store, and the value should be
* the Guardian's Fastly customer id.
*
*/
export class GuFastlyKinesisLogRoleExperimental extends GuRole {
constructor(scope: GuStack, id: string, props: GuFastlyKinesisLogRoleProps) {
const fastlyCustomerId = GuFastlyCustomerIdParameter.getInstance(scope).valueAsString;
const { roleName, stream } = props;

super(scope, id, {
roleName,
assumedBy: new AccountPrincipal(FASTLY_AWS_ACCOUNT_ID),
externalIds: [fastlyCustomerId],
});

const policy = new GuKinesisPutRecordsPolicyExperimental(scope, "GuKinesisPutRecordsPolicy", {
stream,
});

policy.attachToRole(this);
}
}
1 change: 1 addition & 0 deletions src/experimental/constructs/iam/roles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./fastly-kinesis-log";
1 change: 1 addition & 0 deletions src/experimental/constructs/policies/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./kinesis-put-records";
28 changes: 28 additions & 0 deletions src/experimental/constructs/policies/kinesis-put-records.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Template } from "aws-cdk-lib/assertions";
import { GuKinesisStream } from "../../../constructs/kinesis";
import { attachPolicyToTestRole, simpleGuStackForTesting } from "../../../utils/test";
import { GuKinesisPutRecordsPolicyExperimental } from "./kinesis-put-records";

describe("The GuKinesisPutRecordsPolicy class", () => {
it("has the correct action permissions", () => {
const stack = simpleGuStackForTesting();
const stream = new GuKinesisStream(stack, "testStream");

const kinesisPutRecordsPolicy = new GuKinesisPutRecordsPolicyExperimental(stack, "KinesisPolicy", { stream });

attachPolicyToTestRole(stack, kinesisPutRecordsPolicy);

Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", {
PolicyDocument: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["kinesis:PutRecords", "kinesis:ListShards"],
Resource: { "Fn::GetAtt": ["testStream8BCA7523", "Arn"] },
},
],
},
});
});
});
15 changes: 15 additions & 0 deletions src/experimental/constructs/policies/kinesis-put-records.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { GuStack } from "../../../constructs/core";
import type { GuNoStatementsPolicyProps } from "../../../constructs/iam/policies/base-policy";
import { GuAllowPolicy } from "../../../constructs/iam/policies/base-policy";
import type { GuKinesisStream } from "../../../constructs/kinesis";

export interface GuKinesisPutRecordsPolicyProps extends GuNoStatementsPolicyProps {
stream: GuKinesisStream;
}

export class GuKinesisPutRecordsPolicyExperimental extends GuAllowPolicy {
constructor(scope: GuStack, id: string, props: GuKinesisPutRecordsPolicyProps) {
const { stream } = props;
super(scope, id, { actions: ["kinesis:PutRecords", "kinesis:ListShards"], resources: [stream.streamArn] });
}
}

0 comments on commit b018b32

Please sign in to comment.