cdk-rds-sql 7.3.1 → 7.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lambda/handler.ts DELETED
@@ -1,388 +0,0 @@
1
- import {
2
- SecretsManagerClient,
3
- GetSecretValueCommand,
4
- GetSecretValueCommandOutput,
5
- } from "@aws-sdk/client-secrets-manager"
6
- import {
7
- SSMClient,
8
- PutParameterCommand,
9
- DeleteParameterCommand,
10
- AddTagsToResourceCommand,
11
- } from "@aws-sdk/client-ssm"
12
- import {
13
- CloudFormationCustomResourceCreateEvent,
14
- CloudFormationCustomResourceUpdateEvent,
15
- CloudFormationCustomResourceDeleteEvent,
16
- } from "aws-lambda"
17
- import { backOff } from "exponential-backoff"
18
- import { EngineConnectionConfig } from "./engine.abstract"
19
- import { EngineFactory } from "./engine.factory"
20
- import {
21
- ResourceProperties,
22
- CustomResourceResponse,
23
- EngineDatabaseProperties,
24
- EngineRoleProperties,
25
- EngineSchemaProperties,
26
- EngineSqlProperties,
27
- EngineIamGrantProperties,
28
- } from "./types"
29
- import { RdsSqlResource } from "../src/enum"
30
-
31
- const maxAttempts = 20
32
-
33
- const log =
34
- process.env.LOGGER === "true"
35
- ? console.debug
36
- : (_message?: any, ..._optionalParams: any[]) => {}
37
-
38
- const secrets_client = new SecretsManagerClient({})
39
-
40
- // Helper functions to extract engine properties from CloudFormation properties
41
- // These functions throw if the resource type doesn't match the expected type
42
- function toDatabaseEngineProps(props: ResourceProperties): EngineDatabaseProperties {
43
- if (props.Resource !== RdsSqlResource.DATABASE) {
44
- throw new Error(`Expected DATABASE resource, got ${props.Resource}`)
45
- }
46
- return props
47
- }
48
-
49
- function toRoleEngineProps(props: ResourceProperties): EngineRoleProperties {
50
- if (props.Resource !== RdsSqlResource.ROLE) {
51
- throw new Error(`Expected ROLE resource, got ${props.Resource}`)
52
- }
53
- // Convert string 'true'/'false' to boolean for EnableIamAuth
54
- return {
55
- ...props,
56
- EnableIamAuth: props.EnableIamAuth === "true" || props.EnableIamAuth === "1",
57
- }
58
- }
59
-
60
- function toSchemaEngineProps(props: ResourceProperties): EngineSchemaProperties {
61
- if (props.Resource !== RdsSqlResource.SCHEMA) {
62
- throw new Error(`Expected SCHEMA resource, got ${props.Resource}`)
63
- }
64
- return props
65
- }
66
-
67
- function toSqlEngineProps(props: ResourceProperties): EngineSqlProperties {
68
- if (props.Resource !== RdsSqlResource.SQL) {
69
- throw new Error(`Expected SQL resource, got ${props.Resource}`)
70
- }
71
- return props
72
- }
73
-
74
- function toIamGrantEngineProps(props: ResourceProperties): EngineIamGrantProperties {
75
- if (props.Resource !== RdsSqlResource.IAM_GRANT) {
76
- throw new Error(`Expected IAM_GRANT resource, got ${props.Resource}`)
77
- }
78
- return props
79
- }
80
-
81
- export const handler = async (
82
- event:
83
- | CloudFormationCustomResourceCreateEvent<ResourceProperties>
84
- | CloudFormationCustomResourceUpdateEvent<ResourceProperties>
85
- | CloudFormationCustomResourceDeleteEvent<ResourceProperties>
86
- ): Promise<CustomResourceResponse> => {
87
- log(event)
88
-
89
- const requestType = event.RequestType
90
- const resource: RdsSqlResource = event.ResourceProperties.Resource
91
- const resourceId = event.ResourceProperties.ResourceId
92
- const databaseName = event.ResourceProperties.DatabaseName
93
-
94
- if (!Object.values(RdsSqlResource).includes(resource)) {
95
- throw `Resource type '${resource}' not recognised.`
96
- }
97
- // Check if we're dealing with DSQL based on environment variables
98
- const dsqlEndpoint = process.env.DSQL_ENDPOINT
99
- const dsqlPort = process.env.DSQL_PORT
100
-
101
- let engine: string
102
- let secretValues: any = {}
103
-
104
- if (dsqlEndpoint) {
105
- // DSQL configuration - use environment variables
106
- log(`Using DSQL endpoint: ${dsqlEndpoint}`)
107
- engine = "dsql"
108
- secretValues = {
109
- host: dsqlEndpoint,
110
- port: parseInt(dsqlPort || "5432", 10),
111
- username: "admin", // DSQL always uses admin user
112
- engine: "dsql",
113
- }
114
- } else {
115
- // Traditional RDS configuration - use secrets
116
- // Use SecretArn from event properties, fallback to environment variable
117
- const secretArn = event.ResourceProperties.SecretArn || process.env.SECRET_ARN
118
- if (!secretArn) {
119
- throw "SecretArn is required (not in event properties or environment)"
120
- }
121
-
122
- const command = new GetSecretValueCommand({
123
- SecretId: secretArn,
124
- })
125
-
126
- log(`Fetching secret ${secretArn}`)
127
- const secret: GetSecretValueCommandOutput = await backOff(
128
- async () => {
129
- try {
130
- const result = await secrets_client.send(command)
131
- return result
132
- } catch (e) {
133
- log("Error fetching secret %o", e)
134
- throw e
135
- }
136
- },
137
- {
138
- numOfAttempts: 10,
139
- startingDelay: 500,
140
- }
141
- )
142
- if (!secret.SecretString) {
143
- throw `No secret string in ${secretArn}`
144
- }
145
- secretValues = JSON.parse(secret.SecretString)
146
-
147
- // Determine the database engine type
148
- engine = secretValues.engine || "postgresql" // Default to postgresql if not specified
149
- }
150
-
151
- const dbEngine = EngineFactory.createEngine(engine)
152
-
153
- let sql: string | string[] | undefined
154
- switch (requestType) {
155
- case "Create": {
156
- switch (resource) {
157
- case RdsSqlResource.DATABASE:
158
- sql = dbEngine.createDatabase(
159
- resourceId,
160
- toDatabaseEngineProps(event.ResourceProperties)
161
- )
162
- break
163
- case RdsSqlResource.ROLE:
164
- sql = await dbEngine.createRole(
165
- resourceId,
166
- toRoleEngineProps(event.ResourceProperties)
167
- )
168
- break
169
- case RdsSqlResource.SCHEMA:
170
- sql = dbEngine.createSchema(
171
- resourceId,
172
- toSchemaEngineProps(event.ResourceProperties)
173
- )
174
- break
175
- case RdsSqlResource.SQL:
176
- sql = dbEngine.createSql(resourceId, toSqlEngineProps(event.ResourceProperties))
177
- break
178
- case RdsSqlResource.IAM_GRANT:
179
- const createIam = toIamGrantEngineProps(event.ResourceProperties)
180
- sql = dbEngine.createIamGrant(createIam.RoleName, createIam.ResourceArn)
181
- break
182
- case RdsSqlResource.PARAMETER_PASSWORD:
183
- await handleParameterPassword(event.ResourceProperties)
184
- break
185
- }
186
- break
187
- }
188
- case "Update": {
189
- const oldResourceId = (event as CloudFormationCustomResourceUpdateEvent)
190
- .PhysicalResourceId
191
- switch (resource) {
192
- case RdsSqlResource.DATABASE:
193
- const dbProps = toDatabaseEngineProps(event.ResourceProperties)
194
- sql = dbEngine.updateDatabase(resourceId, oldResourceId, {
195
- ...dbProps,
196
- MasterOwner: secretValues.username,
197
- })
198
- break
199
- case RdsSqlResource.ROLE:
200
- const updateEvent =
201
- event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
202
- sql = await dbEngine.updateRole(
203
- resourceId,
204
- oldResourceId,
205
- toRoleEngineProps(event.ResourceProperties),
206
- toRoleEngineProps(updateEvent.OldResourceProperties)
207
- )
208
- break
209
- case RdsSqlResource.SCHEMA:
210
- sql = dbEngine.updateSchema(
211
- resourceId,
212
- oldResourceId,
213
- toSchemaEngineProps(event.ResourceProperties)
214
- )
215
- break
216
- case RdsSqlResource.SQL:
217
- sql = dbEngine.updateSql(
218
- resourceId,
219
- oldResourceId,
220
- toSqlEngineProps(event.ResourceProperties)
221
- )
222
- break
223
- case RdsSqlResource.IAM_GRANT:
224
- const iamUpdateEvent =
225
- event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
226
- const newIam = toIamGrantEngineProps(event.ResourceProperties)
227
- const oldIam = toIamGrantEngineProps(iamUpdateEvent.OldResourceProperties)
228
- sql = dbEngine.updateIamGrant(
229
- newIam.RoleName,
230
- oldIam.RoleName,
231
- newIam.ResourceArn,
232
- oldIam.ResourceArn
233
- )
234
- break
235
- case RdsSqlResource.PARAMETER_PASSWORD:
236
- await handleParameterPassword(event.ResourceProperties)
237
- sql = undefined // No SQL to execute
238
- break
239
- }
240
- break
241
- }
242
- case "Delete": {
243
- switch (resource) {
244
- case RdsSqlResource.DATABASE:
245
- sql = dbEngine.deleteDatabase(resourceId, secretValues.username)
246
- break
247
- case RdsSqlResource.ROLE:
248
- sql = await dbEngine.deleteRole(
249
- resourceId,
250
- toRoleEngineProps(event.ResourceProperties)
251
- )
252
- break
253
- case RdsSqlResource.SCHEMA:
254
- sql = dbEngine.deleteSchema(
255
- resourceId,
256
- toSchemaEngineProps(event.ResourceProperties)
257
- )
258
- break
259
- case RdsSqlResource.SQL:
260
- sql = dbEngine.deleteSql(resourceId, toSqlEngineProps(event.ResourceProperties))
261
- break
262
- case RdsSqlResource.IAM_GRANT:
263
- const delIam = toIamGrantEngineProps(event.ResourceProperties)
264
- sql = dbEngine.deleteIamGrant(delIam.RoleName, delIam.ResourceArn)
265
- break
266
- case RdsSqlResource.PARAMETER_PASSWORD:
267
- await deleteParameterPassword(event.ResourceProperties)
268
- break
269
- }
270
- break
271
- }
272
- }
273
-
274
- if (sql) {
275
- let database: string
276
- if (engine === "dsql") {
277
- // DSQL always uses 'postgres' database
278
- database = "postgres"
279
- } else if (resource === RdsSqlResource.ROLE) {
280
- database = secretValues.dbname
281
- } else {
282
- database = databaseName ?? secretValues.dbname // connect to given database if possible, else to database mentioned in secret
283
- }
284
-
285
- const config: EngineConnectionConfig = {
286
- host: secretValues.host,
287
- port: secretValues.port,
288
- user: secretValues.username,
289
- password: secretValues.password || "", // DSQL will generate token in engine
290
- database: database,
291
- }
292
-
293
- try {
294
- await backOff(
295
- async () => {
296
- return dbEngine.executeSQL(sql as string | string[], config)
297
- },
298
- {
299
- retry: errorFilter,
300
- numOfAttempts: maxAttempts,
301
- }
302
- )
303
- } catch (error) {
304
- log("Error executing SQL: %o", error)
305
- throw error
306
- }
307
- }
308
-
309
- let response: CustomResourceResponse = {}
310
- // Except for the SQL resource, return the new resource id. This
311
- // will cause a delete to be sent for the old resource.
312
- if (resource !== RdsSqlResource.SQL) {
313
- response.PhysicalResourceId = resourceId
314
- }
315
-
316
- return response
317
- }
318
-
319
- // Custom error filter, mainly to retry role creation.
320
- const errorFilter = (error: any, nextAttemptNumber: number) => {
321
- // Retry only if the error message includes "tuple concurrently"
322
- // This will cover concurrent updates and deletes
323
- const willRetry = error.message && error.message.includes("tuple concurrently")
324
- log(
325
- "Encountered an error on attempt %d/%d retry=%s error=[%o]",
326
- nextAttemptNumber - 1,
327
- maxAttempts,
328
- willRetry,
329
- error
330
- )
331
- return willRetry
332
- }
333
-
334
- /**
335
- * Copy password generated in secret to our password parameter.
336
- */
337
- async function handleParameterPassword(props: any): Promise<void> {
338
- const secretArn = props.PasswordArn
339
- const parameterName = props.ParameterName
340
-
341
- // Get the secret
342
- const secretData = await secrets_client.send(
343
- new GetSecretValueCommand({ SecretId: secretArn })
344
- )
345
-
346
- if (!secretData.SecretString) throw "No secret string for parameter"
347
- const secretObj = JSON.parse(secretData.SecretString)
348
-
349
- // Put the password in SSM – retry while IAM perms propagate
350
- const ssmClient = new SSMClient({})
351
- await backOff(
352
- () =>
353
- ssmClient.send(
354
- new PutParameterCommand({
355
- Name: parameterName,
356
- Value: secretObj.password,
357
- Type: "SecureString",
358
- Overwrite: true,
359
- })
360
- ),
361
- {
362
- numOfAttempts: maxAttempts,
363
- startingDelay: 1000,
364
- }
365
- )
366
-
367
- // Tag the parameter so that only parameters created by this construct can be deleted
368
- await ssmClient.send(
369
- new AddTagsToResourceCommand({
370
- ResourceType: "Parameter",
371
- ResourceId: parameterName,
372
- Tags: [{ Key: "created-by", Value: "cdk-rds-sql" }],
373
- })
374
- )
375
- }
376
-
377
- /**
378
- * Cleanup: remove generated password parameter.
379
- */
380
- async function deleteParameterPassword(props: any): Promise<void> {
381
- const parameterName = props.ParameterName
382
- const ssmClient = new SSMClient({})
383
- await ssmClient.send(
384
- new DeleteParameterCommand({
385
- Name: parameterName,
386
- })
387
- )
388
- }
package/lambda/types.ts DELETED
@@ -1,86 +0,0 @@
1
- import { CloudFormationCustomResourceResourcePropertiesCommon } from "aws-lambda"
2
- import { RdsSqlResource } from "../src/enum"
3
-
4
- // Engine-specific interfaces (business logic properties only)
5
- export interface EngineDatabaseProperties {
6
- readonly Owner?: string
7
- readonly MasterOwner?: string
8
- }
9
-
10
- export interface EngineRoleProperties {
11
- readonly PasswordArn?: string
12
- readonly DatabaseName?: string
13
- readonly EnableIamAuth?: boolean
14
- }
15
-
16
- export interface EngineSchemaProperties {
17
- readonly DatabaseName?: string
18
- readonly RoleName?: string
19
- }
20
-
21
- export interface EngineSqlProperties {
22
- readonly DatabaseName?: string
23
- readonly Statement?: string
24
- readonly Rollback?: string
25
- }
26
-
27
- export interface EngineIamGrantProperties {
28
- readonly RoleName: string
29
- readonly ResourceArn: string
30
- }
31
-
32
- // Common CloudFormation properties shared by all resources
33
- export interface CommonProperties
34
- extends CloudFormationCustomResourceResourcePropertiesCommon {
35
- readonly Resource: RdsSqlResource
36
- readonly ResourceId: string
37
- readonly SecretArn?: string // SecretArn is not set for DSQL
38
- }
39
-
40
- // CloudFormation-specific properties (engine properties + CloudFormation metadata)
41
- export interface DatabaseProperties extends CommonProperties, EngineDatabaseProperties {
42
- readonly Resource: RdsSqlResource.DATABASE
43
- }
44
-
45
- export interface RoleProperties extends CommonProperties {
46
- readonly Resource: RdsSqlResource.ROLE
47
- readonly PasswordArn?: string
48
- readonly DatabaseName?: string
49
- readonly EnableIamAuth?: string // CloudFormation passes boolean as string
50
- }
51
-
52
- export interface SchemaProperties extends CommonProperties, EngineSchemaProperties {
53
- readonly Resource: RdsSqlResource.SCHEMA
54
- }
55
-
56
- export interface SqlProperties extends CommonProperties, EngineSqlProperties {
57
- readonly Resource: RdsSqlResource.SQL
58
- }
59
-
60
- export interface IamGrantProperties extends CommonProperties, EngineIamGrantProperties {
61
- readonly Resource: RdsSqlResource.IAM_GRANT
62
- readonly ResourceArn: string
63
- }
64
-
65
- // Parameter password specific properties
66
- export interface ParameterPasswordProperties extends CommonProperties {
67
- readonly Resource: RdsSqlResource.PARAMETER_PASSWORD
68
- readonly PasswordArn: string
69
- readonly ParameterName: string
70
- }
71
-
72
- // Union type of all resource properties
73
- export type ResourceProperties =
74
- | DatabaseProperties
75
- | RoleProperties
76
- | SchemaProperties
77
- | SqlProperties
78
- | IamGrantProperties
79
- | ParameterPasswordProperties
80
-
81
- // Custom resource response interface
82
- export interface CustomResourceResponse {
83
- PhysicalResourceId?: string
84
- Data?: any
85
- NoEcho?: boolean
86
- }
package/lambda/util.ts DELETED
@@ -1,186 +0,0 @@
1
- import {
2
- CloudFormationCustomResourceCreateEvent,
3
- CloudFormationCustomResourceUpdateEvent,
4
- CloudFormationCustomResourceDeleteEvent,
5
- } from "aws-lambda"
6
- import { Client } from "pg"
7
- import { ResourceProperties } from "./types"
8
-
9
- // Helper interface to simplify test request creation
10
- export interface ResourcePropertiesInput {
11
- readonly SecretArn?: string
12
- readonly PasswordArn?: string
13
- readonly DatabaseName?: string
14
- readonly Owner?: string
15
- readonly Statement?: string
16
- readonly Rollback?: string
17
- readonly RoleName?: string
18
- readonly ParameterName?: string
19
- [key: string]: any
20
- }
21
-
22
- /**
23
- * Helpers to create CloudFormation requests.
24
- */
25
- export const createRequest = (
26
- resource: string,
27
- resourceId: string,
28
- props?: Partial<ResourceProperties>
29
- ): CloudFormationCustomResourceCreateEvent<ResourceProperties> => {
30
- return {
31
- ServiceToken: "",
32
- ResponseURL: "",
33
- StackId: "",
34
- RequestId: "",
35
- LogicalResourceId: "",
36
- ResourceType: "",
37
- ResourceProperties: {
38
- ServiceToken: "",
39
- Resource: resource as any, // Needed for backward compatibility with tests
40
- ResourceId: resourceId,
41
- SecretArn:
42
- props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
43
- ...props,
44
- },
45
- RequestType: "Create",
46
- }
47
- }
48
-
49
- export const updateRequest = (
50
- resource: string,
51
- oldResourceId: string,
52
- newResourceId: string,
53
- props?: ResourcePropertiesInput
54
- ): CloudFormationCustomResourceUpdateEvent<ResourceProperties> => {
55
- return {
56
- ServiceToken: "",
57
- ResponseURL: "",
58
- StackId: "",
59
- RequestId: "",
60
- LogicalResourceId: "",
61
- ResourceType: "",
62
- ResourceProperties: {
63
- ServiceToken: "",
64
- Resource: resource as any, // Needed for backward compatibility with tests
65
- ResourceId: newResourceId,
66
- SecretArn:
67
- props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
68
- ...props,
69
- } as unknown as ResourceProperties,
70
- RequestType: "Update",
71
- PhysicalResourceId: oldResourceId,
72
- OldResourceProperties: {
73
- ServiceToken: "",
74
- Resource: resource as any,
75
- ResourceId: oldResourceId,
76
- SecretArn:
77
- props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
78
- } as unknown as ResourceProperties,
79
- }
80
- }
81
-
82
- export const deleteRequest = (
83
- resource: string,
84
- resourceId: string,
85
- props?: ResourcePropertiesInput
86
- ): CloudFormationCustomResourceDeleteEvent<ResourceProperties> => {
87
- return {
88
- ServiceToken: "",
89
- ResponseURL: "",
90
- StackId: "",
91
- RequestId: "",
92
- LogicalResourceId: "",
93
- ResourceType: "",
94
- ResourceProperties: {
95
- ServiceToken: "",
96
- Resource: resource as any, // Needed for backward compatibility with tests
97
- ResourceId: resourceId,
98
- SecretArn:
99
- props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
100
- ...props,
101
- } as unknown as ResourceProperties,
102
- RequestType: "Delete",
103
- PhysicalResourceId: resourceId,
104
- }
105
- }
106
-
107
- /**
108
- * Database helpers.
109
- */
110
- export const schemaExists = async (client: Client, schema: string): Promise<boolean> => {
111
- const schemas = await getSchemas(client)
112
- return schemas.find((s) => s === schema) !== undefined
113
- }
114
-
115
- export const roleGrantedForSchema = async (
116
- client: Client,
117
- schema: string,
118
- role: string
119
- ): Promise<boolean> => {
120
- const sql = `select nspname as schema_name, r.rolname as role_name,\
121
- pg_catalog.has_schema_privilege(r.rolname, nspname, 'CREATE') as create_grant,\
122
- pg_catalog.has_schema_privilege(r.rolname, nspname, 'USAGE') as usage_grant\
123
- from pg_namespace pn,pg_catalog.pg_roles r \
124
- where array_to_string(nspacl,',') like '%'||r.rolname||'%' and nspowner > 1 \
125
- and nspname = '${schema}' and r.rolname = '${role}'`
126
- const { rows } = await client.query(sql)
127
- return (
128
- rows.length === 1 && rows[0].create_grant === true && rows[0].usage_grant === true
129
- )
130
- }
131
-
132
- const getSchemas = async (client: Client): Promise<string[]> => {
133
- const { rows } = await client.query(
134
- "select schema_name from information_schema.schemata"
135
- )
136
- return rows.map((r) => r.schema_name)
137
- }
138
-
139
- export const roleExists = async (client: Client, role: string): Promise<boolean> => {
140
- const roles = await getRoles(client)
141
- return roles.find((r) => r === role) !== undefined
142
- }
143
-
144
- const getRoles = async (client: Client): Promise<string[]> => {
145
- const { rows } = await client.query("select rolname from pg_roles")
146
- return rows.map((r) => r.rolname)
147
- }
148
-
149
- export const databaseExists = async (
150
- client: Client,
151
- database: string
152
- ): Promise<boolean> => {
153
- const databases = await getDatabases(client)
154
- return databases.find((r) => r === database) !== undefined
155
- }
156
-
157
- const getDatabases = async (client: Client): Promise<string[]> => {
158
- const { rows } = await client.query(
159
- "select datname from pg_database where datistemplate = false"
160
- )
161
- return rows.map((r) => r.datname)
162
- }
163
-
164
- export const databaseOwnerIs = async (
165
- client: Client,
166
- database: string,
167
- user_name: string
168
- ): Promise<boolean> => {
169
- const databases = await getDatabasesWithOwner(client, user_name)
170
- return databases.find((r) => r === database) !== undefined
171
- }
172
-
173
- const getDatabasesWithOwner = async (
174
- client: Client,
175
- user_name: string
176
- ): Promise<string[]> => {
177
- const { rows } = await client.query(
178
- `select datname from pg_database where datistemplate = false and pg_catalog.pg_get_userbyid(datdba) = '${user_name}'`
179
- )
180
- return rows.map((r) => r.datname)
181
- }
182
-
183
- export const rowCount = async (client: Client, table_name: string): Promise<number> => {
184
- const { rows } = await client.query(`select count(*) from ${table_name}`)
185
- return parseInt(rows[0].count)
186
- }