cdk-rds-sql 7.3.0 → 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,390 +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
- sql = dbEngine.createIamGrant(
180
- event.ResourceProperties.RoleName,
181
- toIamGrantEngineProps(event.ResourceProperties)
182
- )
183
- break
184
- case RdsSqlResource.PARAMETER_PASSWORD:
185
- await handleParameterPassword(event.ResourceProperties)
186
- break
187
- }
188
- break
189
- }
190
- case "Update": {
191
- const oldResourceId = (event as CloudFormationCustomResourceUpdateEvent)
192
- .PhysicalResourceId
193
- switch (resource) {
194
- case RdsSqlResource.DATABASE:
195
- const dbProps = toDatabaseEngineProps(event.ResourceProperties)
196
- sql = dbEngine.updateDatabase(resourceId, oldResourceId, {
197
- ...dbProps,
198
- MasterOwner: secretValues.username,
199
- })
200
- break
201
- case RdsSqlResource.ROLE:
202
- const updateEvent =
203
- event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
204
- sql = await dbEngine.updateRole(
205
- resourceId,
206
- oldResourceId,
207
- toRoleEngineProps(event.ResourceProperties),
208
- toRoleEngineProps(updateEvent.OldResourceProperties)
209
- )
210
- break
211
- case RdsSqlResource.SCHEMA:
212
- sql = dbEngine.updateSchema(
213
- resourceId,
214
- oldResourceId,
215
- toSchemaEngineProps(event.ResourceProperties)
216
- )
217
- break
218
- case RdsSqlResource.SQL:
219
- sql = dbEngine.updateSql(
220
- resourceId,
221
- oldResourceId,
222
- toSqlEngineProps(event.ResourceProperties)
223
- )
224
- break
225
- case RdsSqlResource.IAM_GRANT:
226
- const iamUpdateEvent =
227
- event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
228
- sql = dbEngine.updateIamGrant(
229
- event.ResourceProperties.RoleName,
230
- iamUpdateEvent.OldResourceProperties.RoleName,
231
- toIamGrantEngineProps(event.ResourceProperties),
232
- toIamGrantEngineProps(iamUpdateEvent.OldResourceProperties)
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
- sql = dbEngine.deleteIamGrant(
264
- event.ResourceProperties.RoleName,
265
- toIamGrantEngineProps(event.ResourceProperties)
266
- )
267
- break
268
- case RdsSqlResource.PARAMETER_PASSWORD:
269
- await deleteParameterPassword(event.ResourceProperties)
270
- break
271
- }
272
- break
273
- }
274
- }
275
-
276
- if (sql) {
277
- let database: string
278
- if (engine === "dsql") {
279
- // DSQL always uses 'postgres' database
280
- database = "postgres"
281
- } else if (resource === RdsSqlResource.ROLE) {
282
- database = secretValues.dbname
283
- } else {
284
- database = databaseName ?? secretValues.dbname // connect to given database if possible, else to database mentioned in secret
285
- }
286
-
287
- const config: EngineConnectionConfig = {
288
- host: secretValues.host,
289
- port: secretValues.port,
290
- user: secretValues.username,
291
- password: secretValues.password || "", // DSQL will generate token in engine
292
- database: database,
293
- }
294
-
295
- try {
296
- await backOff(
297
- async () => {
298
- return dbEngine.executeSQL(sql as string | string[], config)
299
- },
300
- {
301
- retry: errorFilter,
302
- numOfAttempts: maxAttempts,
303
- }
304
- )
305
- } catch (error) {
306
- log("Error executing SQL: %o", error)
307
- throw error
308
- }
309
- }
310
-
311
- let response: CustomResourceResponse = {}
312
- // Except for the SQL resource, return the new resource id. This
313
- // will cause a delete to be sent for the old resource.
314
- if (resource !== RdsSqlResource.SQL) {
315
- response.PhysicalResourceId = resourceId
316
- }
317
-
318
- return response
319
- }
320
-
321
- // Custom error filter, mainly to retry role creation.
322
- const errorFilter = (error: any, nextAttemptNumber: number) => {
323
- // Retry only if the error message includes "tuple concurrently"
324
- // This will cover concurrent updates and deletes
325
- const willRetry = error.message && error.message.includes("tuple concurrently")
326
- log(
327
- "Encountered an error on attempt %d/%d retry=%s error=[%o]",
328
- nextAttemptNumber - 1,
329
- maxAttempts,
330
- willRetry,
331
- error
332
- )
333
- return willRetry
334
- }
335
-
336
- /**
337
- * Copy password generated in secret to our password parameter.
338
- */
339
- async function handleParameterPassword(props: any): Promise<void> {
340
- const secretArn = props.PasswordArn
341
- const parameterName = props.ParameterName
342
-
343
- // Get the secret
344
- const secretData = await secrets_client.send(
345
- new GetSecretValueCommand({ SecretId: secretArn })
346
- )
347
-
348
- if (!secretData.SecretString) throw "No secret string for parameter"
349
- const secretObj = JSON.parse(secretData.SecretString)
350
-
351
- // Put the password in SSM – retry while IAM perms propagate
352
- const ssmClient = new SSMClient({})
353
- await backOff(
354
- () =>
355
- ssmClient.send(
356
- new PutParameterCommand({
357
- Name: parameterName,
358
- Value: secretObj.password,
359
- Type: "SecureString",
360
- Overwrite: true,
361
- })
362
- ),
363
- {
364
- numOfAttempts: maxAttempts,
365
- startingDelay: 1000,
366
- }
367
- )
368
-
369
- // Tag the parameter so that only parameters created by this construct can be deleted
370
- await ssmClient.send(
371
- new AddTagsToResourceCommand({
372
- ResourceType: "Parameter",
373
- ResourceId: parameterName,
374
- Tags: [{ Key: "created-by", Value: "cdk-rds-sql" }],
375
- })
376
- )
377
- }
378
-
379
- /**
380
- * Cleanup: remove generated password parameter.
381
- */
382
- async function deleteParameterPassword(props: any): Promise<void> {
383
- const parameterName = props.ParameterName
384
- const ssmClient = new SSMClient({})
385
- await ssmClient.send(
386
- new DeleteParameterCommand({
387
- Name: parameterName,
388
- })
389
- )
390
- }
package/lambda/types.ts DELETED
@@ -1,85 +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 ResourceArn?: string
29
- }
30
-
31
- // Common CloudFormation properties shared by all resources
32
- export interface CommonProperties
33
- extends CloudFormationCustomResourceResourcePropertiesCommon {
34
- readonly Resource: RdsSqlResource
35
- readonly ResourceId: string
36
- readonly SecretArn: string
37
- }
38
-
39
- // CloudFormation-specific properties (engine properties + CloudFormation metadata)
40
- export interface DatabaseProperties extends CommonProperties, EngineDatabaseProperties {
41
- readonly Resource: RdsSqlResource.DATABASE
42
- }
43
-
44
- export interface RoleProperties extends CommonProperties {
45
- readonly Resource: RdsSqlResource.ROLE
46
- readonly PasswordArn?: string
47
- readonly DatabaseName?: string
48
- readonly EnableIamAuth?: string // CloudFormation passes boolean as string
49
- }
50
-
51
- export interface SchemaProperties extends CommonProperties, EngineSchemaProperties {
52
- readonly Resource: RdsSqlResource.SCHEMA
53
- }
54
-
55
- export interface SqlProperties extends CommonProperties, EngineSqlProperties {
56
- readonly Resource: RdsSqlResource.SQL
57
- }
58
-
59
- export interface IamGrantProperties extends CommonProperties, EngineIamGrantProperties {
60
- readonly Resource: RdsSqlResource.IAM_GRANT
61
- readonly ResourceArn: string
62
- }
63
-
64
- // Parameter password specific properties
65
- export interface ParameterPasswordProperties extends CommonProperties {
66
- readonly Resource: RdsSqlResource.PARAMETER_PASSWORD
67
- readonly PasswordArn: string
68
- readonly ParameterName: string
69
- }
70
-
71
- // Union type of all resource properties
72
- export type ResourceProperties =
73
- | DatabaseProperties
74
- | RoleProperties
75
- | SchemaProperties
76
- | SqlProperties
77
- | IamGrantProperties
78
- | ParameterPasswordProperties
79
-
80
- // Custom resource response interface
81
- export interface CustomResourceResponse {
82
- PhysicalResourceId?: string
83
- Data?: any
84
- NoEcho?: boolean
85
- }
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
- }