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.
@@ -1,121 +0,0 @@
1
- import { ConnectionOptions } from "tls"
2
- import {
3
- SecretsManagerClient,
4
- GetSecretValueCommand,
5
- } from "@aws-sdk/client-secrets-manager"
6
- import {
7
- EngineDatabaseProperties,
8
- EngineRoleProperties,
9
- EngineSchemaProperties,
10
- EngineSqlProperties,
11
- EngineIamGrantProperties,
12
- } from "./types"
13
-
14
- export interface EngineConnectionConfig {
15
- host: string
16
- port: number
17
- user: string
18
- password: string
19
- database: string
20
- ssl?: boolean | ConnectionOptions
21
- }
22
-
23
- export abstract class AbstractEngine {
24
- protected log: (message?: any, ...optionalParams: any[]) => void
25
-
26
- constructor() {
27
- this.log =
28
- process.env.LOGGER === "true"
29
- ? console.debug
30
- : (_message?: any, ..._optionalParams: any[]) => {}
31
- }
32
-
33
- abstract createDatabase(
34
- resourceId: string,
35
- props: EngineDatabaseProperties
36
- ): string | string[]
37
- abstract updateDatabase(
38
- resourceId: string,
39
- oldResourceId: string,
40
- props: EngineDatabaseProperties
41
- ): string | string[]
42
- abstract deleteDatabase(resourceId: string, masterUser: string): string | string[]
43
-
44
- abstract createRole(
45
- resourceId: string,
46
- props: EngineRoleProperties
47
- ): Promise<string | string[]>
48
- abstract updateRole(
49
- resourceId: string,
50
- oldResourceId: string,
51
- props: EngineRoleProperties,
52
- oldProps: EngineRoleProperties
53
- ): Promise<string | string[]>
54
- abstract deleteRole(
55
- resourceId: string,
56
- props: EngineRoleProperties
57
- ): Promise<string | string[]>
58
-
59
- abstract createSchema(
60
- resourceId: string,
61
- props: EngineSchemaProperties
62
- ): string | string[]
63
- abstract updateSchema(
64
- resourceId: string,
65
- oldResourceId: string,
66
- props: EngineSchemaProperties
67
- ): string | string[]
68
- abstract deleteSchema(
69
- resourceId: string,
70
- props: EngineSchemaProperties
71
- ): string | string[]
72
-
73
- abstract createSql(resourceId: string, props: EngineSqlProperties): string | string[]
74
- abstract updateSql(
75
- resourceId: string,
76
- oldResourceId: string,
77
- props: EngineSqlProperties
78
- ): string | string[]
79
- abstract deleteSql(resourceId: string, props: EngineSqlProperties): string | string[]
80
-
81
- abstract createIamGrant(
82
- resourceId: string,
83
- props: EngineIamGrantProperties
84
- ): string | string[]
85
- abstract updateIamGrant(
86
- resourceId: string,
87
- oldResourceId: string,
88
- props: EngineIamGrantProperties,
89
- oldProps: EngineIamGrantProperties
90
- ): string | string[]
91
- abstract deleteIamGrant(
92
- resourceId: string,
93
- props: EngineIamGrantProperties
94
- ): string | string[]
95
-
96
- abstract executeSQL(
97
- sql: string | string[],
98
- config: EngineConnectionConfig
99
- ): Promise<any>
100
-
101
- /**
102
- * Parse password field from secret. Returns void on error or if no password field exists.
103
- */
104
- protected async getPassword(arn: string): Promise<string | void> {
105
- if (!arn) return
106
- const secrets_client = new SecretsManagerClient({
107
- requestHandler: {
108
- connectionTimeout: 5000,
109
- requestTimeout: 10000,
110
- },
111
- })
112
- const command = new GetSecretValueCommand({
113
- SecretId: arn,
114
- })
115
- const secret = await secrets_client.send(command)
116
- if (secret.SecretString) {
117
- const json = JSON.parse(secret.SecretString)
118
- return json.password
119
- }
120
- }
121
- }
@@ -1,386 +0,0 @@
1
- import { DsqlSigner } from "@aws-sdk/dsql-signer"
2
- import { format as pgFormat } from "node-pg-format"
3
- import { Client, ClientConfig } from "pg"
4
- import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract"
5
- import {
6
- EngineDatabaseProperties,
7
- EngineRoleProperties,
8
- EngineSchemaProperties,
9
- EngineSqlProperties,
10
- EngineIamGrantProperties,
11
- } from "./types"
12
-
13
- export class DsqlEngine extends AbstractEngine {
14
- createDatabase(
15
- _resourceId: string,
16
- _props: EngineDatabaseProperties
17
- ): string | string[] {
18
- throw new Error(
19
- "DSQL does not support creating databases. Database is always 'postgres'."
20
- )
21
- }
22
-
23
- updateDatabase(
24
- _resourceId: string,
25
- _oldResourceId: string,
26
- _props: EngineDatabaseProperties
27
- ): string[] {
28
- throw new Error(
29
- "DSQL does not support database operations. Database is always 'postgres'."
30
- )
31
- }
32
-
33
- deleteDatabase(_resourceId: string, _masterUser: string): string | string[] {
34
- throw new Error(
35
- "DSQL does not support deleting databases. Database is always 'postgres'."
36
- )
37
- }
38
-
39
- async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
40
- return this.generateCreateRoleSql(resourceId, props?.DatabaseName)
41
- }
42
-
43
- async updateRole(
44
- resourceId: string,
45
- oldResourceId: string,
46
- props: EngineRoleProperties,
47
- oldProps: EngineRoleProperties
48
- ): Promise<string[]> {
49
- const sql: string[] = []
50
-
51
- if (oldResourceId !== resourceId) {
52
- // DSQL doesn't support RENAME, so we create new role and drop old one
53
- // Create new role with old database permissions first
54
- sql.push(...this.generateCreateRoleSql(resourceId, oldProps?.DatabaseName))
55
-
56
- // Add special marker for IAM grant revocation
57
- sql.push(`-- REVOKE_IAM_GRANTS_FOR_ROLE: ${oldResourceId}`)
58
-
59
- // Drop old role
60
- sql.push(...this.generateDropRoleSql(oldResourceId))
61
-
62
- // If database changed as well, grant new database permissions
63
- if (props?.DatabaseName && props.DatabaseName !== oldProps?.DatabaseName) {
64
- sql.push("BEGIN")
65
- sql.push(
66
- pgFormat("GRANT CONNECT ON DATABASE %I TO %I", props.DatabaseName, resourceId)
67
- )
68
- sql.push("COMMIT")
69
- }
70
- } else {
71
- // Only permissions are changing (no rename)
72
- sql.push("BEGIN")
73
-
74
- // Handle database permission changes
75
- if (
76
- oldProps?.DatabaseName &&
77
- props?.DatabaseName &&
78
- oldProps.DatabaseName !== props.DatabaseName
79
- ) {
80
- // Revoke from old database and grant to new
81
- sql.push(
82
- pgFormat(
83
- "REVOKE CONNECT ON DATABASE %I FROM %I",
84
- oldProps.DatabaseName,
85
- resourceId
86
- )
87
- )
88
- sql.push(
89
- pgFormat("GRANT CONNECT ON DATABASE %I TO %I", props.DatabaseName, resourceId)
90
- )
91
- } else if (props?.DatabaseName && !oldProps?.DatabaseName) {
92
- // Grant to new database
93
- sql.push(
94
- pgFormat("GRANT CONNECT ON DATABASE %I TO %I", props.DatabaseName, resourceId)
95
- )
96
- } else if (!props?.DatabaseName && oldProps?.DatabaseName) {
97
- // Revoke from old database
98
- sql.push(
99
- pgFormat(
100
- "REVOKE CONNECT ON DATABASE %I FROM %I",
101
- oldProps.DatabaseName,
102
- resourceId
103
- )
104
- )
105
- }
106
-
107
- sql.push("COMMIT")
108
- }
109
-
110
- return sql
111
- }
112
-
113
- async deleteRole(
114
- resourceId: string,
115
- props: EngineRoleProperties
116
- ): Promise<string | string[]> {
117
- const sql: string[] = []
118
-
119
- // Add special marker for IAM grant revocation
120
- sql.push(`-- REVOKE_IAM_GRANTS_FOR_ROLE: ${resourceId}`)
121
-
122
- // Add the regular drop role SQL
123
- sql.push(...this.generateDropRoleSql(resourceId, props?.DatabaseName))
124
-
125
- return sql
126
- }
127
-
128
- createSchema(resourceId: string, props: EngineSchemaProperties): string | string[] {
129
- const sql: string[] = [pgFormat("CREATE SCHEMA IF NOT EXISTS %I", resourceId)]
130
- if (props?.RoleName) {
131
- this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
132
- sql.push(stmt)
133
- )
134
- }
135
- return sql
136
- }
137
-
138
- updateSchema(
139
- resourceId: string,
140
- oldResourceId: string,
141
- props: EngineSchemaProperties
142
- ): string | string[] {
143
- const statements: string[] = []
144
- if (props?.RoleName) {
145
- this.revokeRoleFromSchema(oldResourceId, props.RoleName).forEach((stmt) =>
146
- statements.push(stmt)
147
- )
148
- }
149
- if (resourceId !== oldResourceId) {
150
- statements.push(pgFormat("ALTER SCHEMA %I RENAME TO %I", oldResourceId, resourceId))
151
- }
152
- if (props?.RoleName) {
153
- this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
154
- statements.push(stmt)
155
- )
156
- }
157
- return statements
158
- }
159
-
160
- deleteSchema(resourceId: string, props: EngineSchemaProperties): string | string[] {
161
- const statements: string[] = []
162
- if (props?.RoleName) {
163
- this.revokeRoleFromSchema(resourceId, props.RoleName).forEach((stmt) =>
164
- statements.push(stmt)
165
- )
166
- }
167
- statements.push(pgFormat("DROP SCHEMA IF EXISTS %I CASCADE", resourceId))
168
- return statements
169
- }
170
-
171
- createSql(_resourceId: string, props: EngineSqlProperties): string | string[] {
172
- if (!props.Statement) {
173
- throw new Error("Statement is required for SQL resource")
174
- }
175
- return props.Statement
176
- }
177
-
178
- updateSql(
179
- _resourceId: string,
180
- _oldResourceId: string,
181
- props: EngineSqlProperties
182
- ): string | string[] {
183
- if (!props.Statement) {
184
- throw new Error("Statement is required for SQL resource")
185
- }
186
- return props.Statement
187
- }
188
-
189
- deleteSql(_resourceId: string, props: EngineSqlProperties): string | string[] {
190
- if (props.Rollback) {
191
- return props.Rollback
192
- }
193
- return []
194
- }
195
-
196
- createIamGrant(resourceId: string, props: EngineIamGrantProperties): string | string[] {
197
- if (!props.ResourceArn) {
198
- throw new Error("ResourceArn is required for IAM grant")
199
- }
200
- return pgFormat("AWS IAM GRANT %I TO %L", resourceId, props.ResourceArn)
201
- }
202
-
203
- updateIamGrant(
204
- resourceId: string,
205
- oldResourceId: string,
206
- props: EngineIamGrantProperties,
207
- oldProps: EngineIamGrantProperties
208
- ): string | string[] {
209
- const statements: string[] = []
210
-
211
- // Revoke old grant if resource ARN or role name changed
212
- if (oldProps.ResourceArn) {
213
- statements.push(
214
- pgFormat("AWS IAM REVOKE %I FROM %L", oldResourceId, oldProps.ResourceArn)
215
- )
216
- }
217
-
218
- // Grant new permissions
219
- if (props.ResourceArn) {
220
- statements.push(pgFormat("AWS IAM GRANT %I TO %L", resourceId, props.ResourceArn))
221
- }
222
-
223
- return statements
224
- }
225
-
226
- deleteIamGrant(resourceId: string, props: EngineIamGrantProperties): string | string[] {
227
- if (!props.ResourceArn) {
228
- throw new Error("ResourceArn is required for IAM grant deletion")
229
- }
230
- return pgFormat("AWS IAM REVOKE %I FROM %L", resourceId, props.ResourceArn)
231
- }
232
-
233
- async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
234
- this.log("Connecting to DSQL cluster...")
235
-
236
- // For DSQL, we need to generate an IAM auth token
237
- const region = process.env.AWS_REGION
238
- if (!region) {
239
- throw new Error("AWS_REGION environment variable is required for DSQL")
240
- }
241
-
242
- const signer = new DsqlSigner({
243
- hostname: config.host,
244
- region: region,
245
- })
246
- const authToken = await signer.getDbConnectAdminAuthToken()
247
-
248
- const clientConfig: ClientConfig = {
249
- host: config.host,
250
- port: config.port,
251
- user: "admin", // DSQL always uses 'admin' user
252
- password: authToken, // Use IAM auth token as password
253
- database: "postgres", // DSQL always uses 'postgres' database
254
- ssl: {
255
- //ca: fs.readFileSync(`${process.env.LAMBDA_TASK_ROOT}/global-bundle.pem`),
256
- rejectUnauthorized: true,
257
- },
258
- connectionTimeoutMillis: 30000,
259
- query_timeout: 30000,
260
- //statement_timeout: 30000, // not supported on DSQL
261
- }
262
-
263
- const client = new Client(clientConfig)
264
-
265
- try {
266
- await client.connect()
267
- this.log("Connected to DSQL cluster")
268
-
269
- const statements = Array.isArray(sql) ? sql : [sql]
270
- const results = []
271
-
272
- for (const statement of statements) {
273
- if (statement.trim()) {
274
- // Handle special IAM grant revocation marker
275
- if (statement.startsWith("-- REVOKE_IAM_GRANTS_FOR_ROLE:")) {
276
- const roleName = statement
277
- .replace("-- REVOKE_IAM_GRANTS_FOR_ROLE:", "")
278
- .trim()
279
- await this.revokeIamGrantsForRole(client, roleName)
280
- } else if (statement.includes("AWS IAM REVOKE")) {
281
- this.log(`Executing SQL: ${statement}`)
282
- try {
283
- const result = await client.query(statement)
284
- results.push(result)
285
- } catch (error: any) {
286
- if (error.message && error.message.includes("does not exist")) {
287
- this.log(
288
- `Ignoring error for IAM REVOKE (role doesn't exist): ${error.message}`
289
- )
290
- } else {
291
- throw error
292
- }
293
- }
294
- } else {
295
- this.log(`Executing SQL: ${statement}`)
296
- const result = await client.query(statement)
297
- results.push(result)
298
- }
299
- }
300
- }
301
-
302
- this.log("SQL execution completed")
303
- return results.length === 1 ? results[0] : results
304
- } catch (error) {
305
- this.log(`Error executing SQL: ${error}`)
306
- throw error
307
- } finally {
308
- try {
309
- await client.end()
310
- this.log("Disconnected from DSQL cluster")
311
- } catch (error) {
312
- this.log(`Error disconnecting: ${error}`)
313
- }
314
- }
315
- }
316
-
317
- private grantRoleForSchema(schema: string, roleName: string): string[] {
318
- return [
319
- pgFormat("GRANT USAGE ON SCHEMA %I TO %I", schema, roleName),
320
- pgFormat("GRANT CREATE ON SCHEMA %I TO %I", schema, roleName),
321
- ]
322
- }
323
-
324
- private revokeRoleFromSchema(schema: string, roleName: string): string[] {
325
- return [
326
- pgFormat("REVOKE CREATE ON SCHEMA %I FROM %I", schema, roleName),
327
- pgFormat("REVOKE ALL ON SCHEMA %I FROM %I", schema, roleName),
328
- ]
329
- }
330
-
331
- private generateCreateRoleSql(roleName: string, databaseName?: string): string[] {
332
- const sql = ["BEGIN"]
333
- sql.push(pgFormat("CREATE ROLE %I WITH LOGIN", roleName))
334
-
335
- if (databaseName) {
336
- sql.push(pgFormat("GRANT CONNECT ON DATABASE %I TO %I", databaseName, roleName))
337
- }
338
-
339
- sql.push("COMMIT")
340
- return sql
341
- }
342
-
343
- private generateDropRoleSql(roleName: string, databaseName?: string): string[] {
344
- const sql = ["BEGIN"]
345
-
346
- if (databaseName) {
347
- sql.push(
348
- pgFormat("REVOKE ALL PRIVILEGES ON DATABASE %I FROM %I", databaseName, roleName)
349
- )
350
- }
351
-
352
- sql.push(pgFormat("DROP ROLE IF EXISTS %I", roleName))
353
- sql.push("COMMIT")
354
- return sql
355
- }
356
-
357
- private async revokeIamGrantsForRole(client: any, roleName: string): Promise<void> {
358
- try {
359
- this.log(`Querying IAM grants for role: ${roleName}`)
360
-
361
- // Query the IAM role mappings for this role
362
- const result = await client.query(
363
- pgFormat(
364
- "SELECT arn FROM sys.iam_pg_role_mappings WHERE pg_role_name = %L",
365
- roleName
366
- )
367
- )
368
-
369
- if (result.rows && result.rows.length > 0) {
370
- this.log(`Found ${result.rows.length} IAM grants to revoke for role: ${roleName}`)
371
-
372
- // Revoke each IAM grant
373
- for (const row of result.rows) {
374
- const revokeStatement = pgFormat("AWS IAM REVOKE %I FROM %L", roleName, row.arn)
375
- this.log(`Executing IAM revoke: ${revokeStatement}`)
376
- await client.query(revokeStatement)
377
- }
378
- } else {
379
- this.log(`No IAM grants found for role: ${roleName}`)
380
- }
381
- } catch (error) {
382
- this.log(`Error revoking IAM grants for role ${roleName}: ${error}`)
383
- // Don't throw here - the role might not have any IAM grants, which is fine
384
- }
385
- }
386
- }
@@ -1,23 +0,0 @@
1
- import { AbstractEngine } from "./engine.abstract"
2
- import { DsqlEngine } from "./engine.dsql"
3
- import { MysqlEngine } from "./engine.mysql"
4
- import { PostgresqlEngine } from "./engine.postgresql"
5
-
6
- export class EngineFactory {
7
- static createEngine(engine: string): AbstractEngine {
8
- switch (engine.toLowerCase()) {
9
- case "dsql":
10
- return new DsqlEngine()
11
- case "postgres":
12
- case "postgresql":
13
- case "aurora-postgresql":
14
- return new PostgresqlEngine()
15
- case "mysql":
16
- case "mariadb":
17
- case "aurora-mysql":
18
- return new MysqlEngine()
19
- default:
20
- throw new Error(`Unsupported database engine: ${engine}`)
21
- }
22
- }
23
- }