Skip to content

Commit

Permalink
fix: Ensure RedshiftServerlessNamespace uses adminUsername prop
Browse files Browse the repository at this point in the history
  • Loading branch information
BDQ committed Jan 27, 2025
1 parent c3539c4 commit 9eec689
Showing 1 changed file with 115 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { CustomResource, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
import { Effect, IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';
import { IFunction } from 'aws-cdk-lib/aws-lambda';
import { ILogGroup } from 'aws-cdk-lib/aws-logs';
import { ISecret, Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { Construct } from 'constructs';
import { RedshiftServerlessNamespaceProps } from './redshift-serverless-namespace-props';
import { Context, CreateServiceLinkedRole, TrackedConstruct, TrackedConstructProps, Utils } from '../../../../utils';
import { DsfProvider } from '../../../../utils/lib/dsf-provider';
import { ServiceLinkedRoleService } from '../../../../utils/lib/service-linked-role-service';
import { CustomResource, Duration, RemovalPolicy, Stack } from "aws-cdk-lib";
import {
Effect,
IRole,
PolicyDocument,
PolicyStatement,
Role,
ServicePrincipal,
} from "aws-cdk-lib/aws-iam";
import { IKey, Key } from "aws-cdk-lib/aws-kms";
import { IFunction } from "aws-cdk-lib/aws-lambda";
import { ILogGroup } from "aws-cdk-lib/aws-logs";
import { ISecret, Secret } from "aws-cdk-lib/aws-secretsmanager";
import { Construct } from "constructs";
import { RedshiftServerlessNamespaceProps } from "./redshift-serverless-namespace-props";
import {
Context,
CreateServiceLinkedRole,
TrackedConstruct,
TrackedConstructProps,
Utils,
} from "../../../../utils";
import { DsfProvider } from "../../../../utils/lib/dsf-provider";
import { ServiceLinkedRoleService } from "../../../../utils/lib/service-linked-role-service";

/**
* Create a Redshift Serverless Namespace with the admin credentials stored in Secrets Manager
Expand All @@ -23,7 +36,6 @@ import { ServiceLinkedRoleService } from '../../../../utils/lib/service-linked-r
* });
*/
export class RedshiftServerlessNamespace extends TrackedConstruct {

/**
* The custom resource that creates the Namespace
*/
Expand Down Expand Up @@ -92,9 +104,13 @@ export class RedshiftServerlessNamespace extends TrackedConstruct {
* Used for convenient access to Stack related information such as region and account id.
*/
private readonly currentStack: Stack;
private namespaceParameters: { [key:string]: any };
private namespaceParameters: { [key: string]: any };

constructor(scope: Construct, id: string, props: RedshiftServerlessNamespaceProps) {
constructor(
scope: Construct,
id: string,
props: RedshiftServerlessNamespaceProps
) {
const trackedConstructProps: TrackedConstructProps = {
trackingTag: RedshiftServerlessNamespace.name,
};
Expand All @@ -114,26 +130,45 @@ export class RedshiftServerlessNamespace extends TrackedConstruct {
this.roles[props.defaultIAMRole.roleArn] = props.defaultIAMRole;
}

this.removalPolicy = Context.revertRemovalPolicy(scope, props.removalPolicy);
this.removalPolicy = Context.revertRemovalPolicy(
scope,
props.removalPolicy
);

const slr = props.serviceLinkedRoleFactory || new CreateServiceLinkedRole(this, 'CreateSLR', {
removalPolicy: this.removalPolicy,
});
const slr =
props.serviceLinkedRoleFactory ||
new CreateServiceLinkedRole(this, "CreateSLR", {
removalPolicy: this.removalPolicy,
});
slr.create(ServiceLinkedRoleService.REDSHIFT);

this.dbName = props.dbName;
const logExports: string[] = props.logExports || [];
this.namespaceName = `${props.name}-${Utils.generateUniqueHash(this)}`;
this.dataKey = props.dataKey ?? new Key(this, 'DefaultNamespaceKey', { enableKeyRotation: true, removalPolicy: this.removalPolicy });
this.adminSecretKey = props.adminSecretKey ?? new Key(this, 'DefaultManagedAdminPasswordKey', { enableKeyRotation: true, removalPolicy: this.removalPolicy });
this.dataKey =
props.dataKey ??
new Key(this, "DefaultNamespaceKey", {
enableKeyRotation: true,
removalPolicy: this.removalPolicy,
});
this.adminSecretKey =
props.adminSecretKey ??
new Key(this, "DefaultManagedAdminPasswordKey", {
enableKeyRotation: true,
removalPolicy: this.removalPolicy,
});
const namespaceArn = `arn:aws:redshift-serverless:${this.currentStack.region}:${this.currentStack.account}:namespace/*`;
const indexParameterName = `updateNamespace-idx-${Utils.generateUniqueHash(this)}`;
const indexParameterName = `updateNamespace-idx-${Utils.generateUniqueHash(
this
)}`;
this.namespaceParameters = {
namespaceName: this.namespaceName,
managedAdminPasswordKeyId: this.adminSecretKey.keyId,
adminUsername: 'admin',
adminUsername: props.adminUsername || "admin",
dbName: props.dbName,
defaultIamRoleArn: props.defaultIAMRole ? props.defaultIAMRole.roleArn : undefined,
defaultIamRoleArn: props.defaultIAMRole
? props.defaultIAMRole.roleArn
: undefined,
iamRoles: this.roles ? Object.keys(this.roles) : undefined,
kmsKeyId: this.dataKey.keyId,
manageAdminPassword: true,
Expand All @@ -150,9 +185,9 @@ export class RedshiftServerlessNamespace extends TrackedConstruct {
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'ssm:GetParameter',
'ssm:PutParameter',
'ssm:DeleteParameter',
"ssm:GetParameter",
"ssm:PutParameter",
"ssm:DeleteParameter",
],
resources: [
`arn:aws:ssm:${this.currentStack.region}:${this.currentStack.account}:parameter/${indexParameterName}`,
Expand All @@ -161,97 +196,103 @@ export class RedshiftServerlessNamespace extends TrackedConstruct {
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'redshift-serverless:CreateNamespace',
'redshift-serverless:GetNamespace',
'redshift-serverless:UpdateNamespace',
'redshift-serverless:DeleteNamespace',
"redshift-serverless:CreateNamespace",
"redshift-serverless:GetNamespace",
"redshift-serverless:UpdateNamespace",
"redshift-serverless:DeleteNamespace",
],
resources: [namespaceArn],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'secretsmanager:CreateSecret',
'secretsmanager:TagResource',
'secretsmanager:DeleteSecret',
"secretsmanager:CreateSecret",
"secretsmanager:TagResource",
"secretsmanager:DeleteSecret",
],
resources: [
`arn:aws:secretsmanager:${this.currentStack.region}:${this.currentStack.account}:secret:redshift!*`,
],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'secretsmanager:RotateSecret',
],
actions: ["secretsmanager:RotateSecret"],
resources: [
`arn:aws:secretsmanager:${this.currentStack.region}:${this.currentStack.account}:secret:redshift!*`,
],
conditions: {
StringEquals: {
'aws:ResourceTag/aws:secretsmanager:owningService': 'redshift',
"aws:ResourceTag/aws:secretsmanager:owningService": "redshift",
},
},
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'kms:Decrypt',
'kms:Encrypt',
'kms:ReEncrypt*',
'kms:GenerateDataKey*',
'kms:DescribeKey',
'kms:CreateGrant',
"kms:Decrypt",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey",
"kms:CreateGrant",
],
resources: [this.dataKey.keyArn, this.adminSecretKey.keyArn],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'iam:CreateServiceLinkedRole',
actions: ["iam:CreateServiceLinkedRole"],
resources: [
`arn:aws:iam::${
Stack.of(this).account
}:role/aws-service-role/redshift.amazonaws.com/AWSServiceRoleForRedshift`,
],
resources: [`arn:aws:iam::${Stack.of(this).account}:role/aws-service-role/redshift.amazonaws.com/AWSServiceRoleForRedshift`],
conditions: {
StringEquals: {
'iam:AWSServiceName': 'redshift.amazonaws.com',
"iam:AWSServiceName": "redshift.amazonaws.com",
},
},
}),
];

// If there are IAM Roles to configure in the namespace, we grant pass role for these roles to the custom resource
if (roleArns && roleArns.length > 0) {
createNamespaceCrPolicyStatements.push(new PolicyStatement({
effect: Effect.ALLOW,
actions: [
'iam:PassRole',
],
resources: roleArns,
}));
createNamespaceCrPolicyStatements.push(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["iam:PassRole"],
resources: roleArns,
})
);
}

const namespaceCrRole = new Role(this, 'ManagementRole', {
assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
const namespaceCrRole = new Role(this, "ManagementRole", {
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
inlinePolicies: {
PrimaryPermissions: new PolicyDocument({
statements: createNamespaceCrPolicyStatements,
}),
},
});

const provider = new DsfProvider(this, 'Provider', {
providerName: 'RedshiftServerlessNamespace',
const provider = new DsfProvider(this, "Provider", {
providerName: "RedshiftServerlessNamespace",
onEventHandlerDefinition: {
depsLockFilePath: __dirname+'/../resources/RedshiftServerlessNamespace/package-lock.json',
entryFile: __dirname+'/../resources/RedshiftServerlessNamespace/index.mjs',
handler: 'index.handler',
depsLockFilePath:
__dirname +
"/../resources/RedshiftServerlessNamespace/package-lock.json",
entryFile:
__dirname + "/../resources/RedshiftServerlessNamespace/index.mjs",
handler: "index.handler",
iamRole: namespaceCrRole,
timeout: Duration.minutes(5),
},
isCompleteHandlerDefinition: {
depsLockFilePath: __dirname+'/../resources/RedshiftServerlessNamespace/package-lock.json',
entryFile: __dirname+'/../resources/RedshiftServerlessNamespace/index.mjs',
handler: 'index.isCompleteHandler',
depsLockFilePath:
__dirname +
"/../resources/RedshiftServerlessNamespace/package-lock.json",
entryFile:
__dirname + "/../resources/RedshiftServerlessNamespace/index.mjs",
handler: "index.isCompleteHandler",
iamRole: namespaceCrRole,
timeout: Duration.minutes(5),
},
Expand All @@ -267,8 +308,8 @@ export class RedshiftServerlessNamespace extends TrackedConstruct {
this.statusFunction = provider.isCompleteHandlerFunction!;
this.statusRole = provider.isCompleteHandlerRole!;

this.customResource = new CustomResource(this, 'CustomResource', {
resourceType: 'Custom::RedshiftServerlessNamespace',
this.customResource = new CustomResource(this, "CustomResource", {
resourceType: "Custom::RedshiftServerlessNamespace",
serviceToken: provider.serviceToken,
properties: this.namespaceParameters,
removalPolicy: this.removalPolicy,
Expand All @@ -277,8 +318,12 @@ export class RedshiftServerlessNamespace extends TrackedConstruct {
this.customResource.node.addDependency(this.dataKey);
this.customResource.node.addDependency(this.adminSecretKey);

this.adminSecret = Secret.fromSecretCompleteArn(this, 'ManagedSecret', this.customResource.getAttString('adminPasswordSecretArn'));
this.namespaceId = this.customResource.getAttString('namespaceId');
this.namespaceArn = this.customResource.getAttString('namespaceArn');
this.adminSecret = Secret.fromSecretCompleteArn(
this,
"ManagedSecret",
this.customResource.getAttString("adminPasswordSecretArn")
);
this.namespaceId = this.customResource.getAttString("namespaceId");
this.namespaceArn = this.customResource.getAttString("namespaceArn");
}
}
}

0 comments on commit 9eec689

Please sign in to comment.