cdk-rds-sql 7.3.1 → 8.0.0
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/.jsii +16 -16
- package/README.md +36 -11
- package/lib/database.js +1 -1
- package/lib/handler/handler.js +28 -21
- package/lib/iam-grant.d.ts +0 -1
- package/lib/iam-grant.js +2 -12
- package/lib/provider.js +1 -1
- package/lib/role.d.ts +10 -4
- package/lib/role.js +56 -53
- package/lib/schema.js +1 -1
- package/lib/sql.js +1 -1
- package/package.json +1 -1
- package/lambda/engine.abstract.ts +0 -114
- package/lambda/engine.dsql.ts +0 -391
- package/lambda/engine.factory.ts +0 -23
- package/lambda/engine.mysql.test.ts +0 -228
- package/lambda/engine.mysql.ts +0 -249
- package/lambda/engine.postgresql.test.ts +0 -151
- package/lambda/engine.postgresql.ts +0 -329
- package/lambda/handler.mysql.test.ts +0 -349
- package/lambda/handler.postgresql.test.ts +0 -338
- package/lambda/handler.test.ts +0 -164
- package/lambda/handler.ts +0 -388
- package/lambda/types.ts +0 -86
- package/lambda/util.ts +0 -186
package/lambda/engine.mysql.ts
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs"
|
|
2
|
-
import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract"
|
|
3
|
-
import {
|
|
4
|
-
EngineDatabaseProperties,
|
|
5
|
-
EngineRoleProperties,
|
|
6
|
-
EngineSchemaProperties,
|
|
7
|
-
EngineSqlProperties,
|
|
8
|
-
} from "./types"
|
|
9
|
-
|
|
10
|
-
export class MysqlEngine extends AbstractEngine {
|
|
11
|
-
createDatabase(resourceId: string, props: EngineDatabaseProperties): string[] {
|
|
12
|
-
const sql = [`CREATE DATABASE IF NOT EXISTS \`${resourceId}\``]
|
|
13
|
-
|
|
14
|
-
if (props.Owner) {
|
|
15
|
-
sql.push(`GRANT ALL PRIVILEGES ON \`${resourceId}\`.* TO '${props.Owner}'@'%'`)
|
|
16
|
-
sql.push("FLUSH PRIVILEGES")
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return sql
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
updateDatabase(): string[] {
|
|
23
|
-
throw new Error("Renaming database is not supported in MySQL.")
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
deleteDatabase(resourceId: string, _masterUser: string): string[] {
|
|
27
|
-
return [`DROP DATABASE IF EXISTS \`${resourceId}\``]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
|
|
31
|
-
const sql: string[] = []
|
|
32
|
-
|
|
33
|
-
if (props.EnableIamAuth) {
|
|
34
|
-
// Create user for IAM authentication
|
|
35
|
-
sql.push(
|
|
36
|
-
`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`
|
|
37
|
-
)
|
|
38
|
-
} else {
|
|
39
|
-
// Create user with password authentication
|
|
40
|
-
if (!props.PasswordArn) throw new Error("No PasswordArn provided")
|
|
41
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
42
|
-
if (!password) throw `Cannot parse password from ${props.PasswordArn}`
|
|
43
|
-
|
|
44
|
-
sql.push(
|
|
45
|
-
`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'`
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (props.DatabaseName) {
|
|
50
|
-
sql.push(
|
|
51
|
-
`GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'`
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
sql.push("FLUSH PRIVILEGES")
|
|
56
|
-
return sql
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async updateRole(
|
|
60
|
-
resourceId: string,
|
|
61
|
-
oldResourceId: string,
|
|
62
|
-
props: EngineRoleProperties,
|
|
63
|
-
oldProps: EngineRoleProperties
|
|
64
|
-
): Promise<string[]> {
|
|
65
|
-
const sql: string[] = []
|
|
66
|
-
|
|
67
|
-
if (oldResourceId !== resourceId) {
|
|
68
|
-
// MySQL doesn't allow renaming users directly, we need to create a new one and drop the old one
|
|
69
|
-
if (props.EnableIamAuth) {
|
|
70
|
-
// Create new user with IAM auth
|
|
71
|
-
sql.push(
|
|
72
|
-
`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`
|
|
73
|
-
)
|
|
74
|
-
} else if (props?.PasswordArn) {
|
|
75
|
-
// Create new user with password auth
|
|
76
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
77
|
-
if (!password) throw `Cannot parse password from ${props.PasswordArn}`
|
|
78
|
-
|
|
79
|
-
sql.push(
|
|
80
|
-
`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'`
|
|
81
|
-
)
|
|
82
|
-
} else {
|
|
83
|
-
// If no password is provided, create user with a random password then expire it
|
|
84
|
-
sql.push(`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY UUID()`)
|
|
85
|
-
sql.push(`ALTER USER '${resourceId}'@'%' PASSWORD EXPIRE`)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Drop the old user
|
|
89
|
-
sql.push(`DROP USER IF EXISTS '${oldResourceId}'@'%'`)
|
|
90
|
-
} else {
|
|
91
|
-
// Handle authentication method changes for existing user
|
|
92
|
-
if (props?.EnableIamAuth && !oldProps?.EnableIamAuth) {
|
|
93
|
-
// Switching from password to IAM auth - need to recreate user
|
|
94
|
-
sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`)
|
|
95
|
-
sql.push(
|
|
96
|
-
`CREATE USER '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`
|
|
97
|
-
)
|
|
98
|
-
} else if (!props?.EnableIamAuth && oldProps?.EnableIamAuth) {
|
|
99
|
-
// Switching from IAM to password auth - need to recreate user
|
|
100
|
-
sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`)
|
|
101
|
-
if (props?.PasswordArn) {
|
|
102
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
103
|
-
if (!password) throw `Cannot parse password from ${props.PasswordArn}`
|
|
104
|
-
sql.push(`CREATE USER '${resourceId}'@'%' IDENTIFIED BY '${password}'`)
|
|
105
|
-
}
|
|
106
|
-
} else if (!props?.EnableIamAuth && props?.PasswordArn) {
|
|
107
|
-
// Just update the password for password auth
|
|
108
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
109
|
-
if (!password) throw `Cannot parse password from ${props.PasswordArn}`
|
|
110
|
-
sql.push(`ALTER USER '${resourceId}'@'%' IDENTIFIED BY '${password}'`)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Check if database name has changed
|
|
115
|
-
if (
|
|
116
|
-
oldProps?.DatabaseName &&
|
|
117
|
-
props?.DatabaseName &&
|
|
118
|
-
oldProps.DatabaseName !== props.DatabaseName
|
|
119
|
-
) {
|
|
120
|
-
// Revoke from old database
|
|
121
|
-
sql.push(
|
|
122
|
-
`REVOKE ALL PRIVILEGES ON \`${oldProps.DatabaseName}\`.* FROM '${resourceId}'@'%'`
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (props?.DatabaseName) {
|
|
127
|
-
sql.push(
|
|
128
|
-
`GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'`
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (sql.length > 0) {
|
|
133
|
-
sql.push("FLUSH PRIVILEGES")
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return sql
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async deleteRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
|
|
140
|
-
const sql: string[] = []
|
|
141
|
-
|
|
142
|
-
if (props?.DatabaseName) {
|
|
143
|
-
sql.push(
|
|
144
|
-
`REVOKE ALL PRIVILEGES ON \`${props.DatabaseName}\`.* FROM '${resourceId}'@'%'`
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`)
|
|
149
|
-
sql.push("FLUSH PRIVILEGES")
|
|
150
|
-
|
|
151
|
-
return sql
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
createSchema(_resourceId: string, _props: EngineSchemaProperties): string[] {
|
|
155
|
-
throw new Error("Schemas are not supported in MySQL/MariaDB")
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
updateSchema(
|
|
159
|
-
_resourceId: string,
|
|
160
|
-
_oldResourceId: string,
|
|
161
|
-
_props: EngineSchemaProperties
|
|
162
|
-
): string[] {
|
|
163
|
-
throw new Error("Schemas are not supported in MySQL/MariaDB")
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
deleteSchema(_resourceId: string, _props: EngineSchemaProperties): string[] {
|
|
167
|
-
throw new Error("Schemas are not supported in MySQL/MariaDB")
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
createSql(_resourceId: string, props: EngineSqlProperties): string {
|
|
171
|
-
return props?.Statement || ""
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
updateSql(
|
|
175
|
-
_resourceId: string,
|
|
176
|
-
_oldResourceId: string,
|
|
177
|
-
props: EngineSqlProperties
|
|
178
|
-
): string {
|
|
179
|
-
return props?.Statement || ""
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
deleteSql(_resourceId: string, props: EngineSqlProperties): string {
|
|
183
|
-
return props?.Rollback || ""
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
createIamGrant(_roleName: string, _iamArn: string): string | string[] {
|
|
187
|
-
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
updateIamGrant(
|
|
191
|
-
_roleName: string,
|
|
192
|
-
_oldRoleName: string,
|
|
193
|
-
_iamArn: string,
|
|
194
|
-
_oldIamArn: string
|
|
195
|
-
): string | string[] {
|
|
196
|
-
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
deleteIamGrant(_roleName: string, _iamArn: string): string | string[] {
|
|
200
|
-
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
|
|
204
|
-
// Dynamic import to avoid bundling issues
|
|
205
|
-
const { createConnection } = await import("mysql2/promise")
|
|
206
|
-
|
|
207
|
-
const isSslEnabled = process.env.SSL ? JSON.parse(process.env.SSL) : true
|
|
208
|
-
|
|
209
|
-
const sslOptions = isSslEnabled
|
|
210
|
-
? {
|
|
211
|
-
ssl: {
|
|
212
|
-
ca: fs.readFileSync(`${process.env.LAMBDA_TASK_ROOT}/global-bundle.pem`),
|
|
213
|
-
rejectUnauthorized: true,
|
|
214
|
-
},
|
|
215
|
-
}
|
|
216
|
-
: {}
|
|
217
|
-
|
|
218
|
-
const connectionConfig = {
|
|
219
|
-
host: config.host,
|
|
220
|
-
port: config.port,
|
|
221
|
-
user: config.user,
|
|
222
|
-
password: config.password,
|
|
223
|
-
database: config.database,
|
|
224
|
-
connectTimeout: 30000,
|
|
225
|
-
multipleStatements: true,
|
|
226
|
-
...sslOptions,
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.log(
|
|
230
|
-
`Connecting to MySQL/MariaDB host ${connectionConfig.host}:${
|
|
231
|
-
connectionConfig.port
|
|
232
|
-
}${isSslEnabled ? " using a secure connection" : ""}, database ${
|
|
233
|
-
connectionConfig.database
|
|
234
|
-
} as ${connectionConfig.user}`
|
|
235
|
-
)
|
|
236
|
-
this.log("Executing SQL", sql)
|
|
237
|
-
|
|
238
|
-
const connection = await createConnection(connectionConfig)
|
|
239
|
-
try {
|
|
240
|
-
if (typeof sql === "string") {
|
|
241
|
-
return await connection.query(sql)
|
|
242
|
-
} else if (sql) {
|
|
243
|
-
return await Promise.all(sql.map((statement) => connection.query(statement)))
|
|
244
|
-
}
|
|
245
|
-
} finally {
|
|
246
|
-
await connection.end()
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { PostgresqlEngine } from "./engine.postgresql"
|
|
2
|
-
|
|
3
|
-
describe("PostgreSQL Engine", () => {
|
|
4
|
-
let engine: PostgresqlEngine
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
engine = new PostgresqlEngine()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
describe("Role", () => {
|
|
11
|
-
it("should generate SQL to revoke privileges on old database when database is changed", async () => {
|
|
12
|
-
// Mock getPassword implementation
|
|
13
|
-
jest.spyOn(engine as any, "getPassword").mockResolvedValue("test-password")
|
|
14
|
-
|
|
15
|
-
const oldProps = {
|
|
16
|
-
DatabaseName: "olddb",
|
|
17
|
-
PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const newProps = {
|
|
21
|
-
DatabaseName: "newdb",
|
|
22
|
-
PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const sql = await engine.updateRole("testrole", "testrole", newProps, oldProps)
|
|
26
|
-
|
|
27
|
-
expect(Array.isArray(sql)).toBe(true)
|
|
28
|
-
|
|
29
|
-
// Check for transaction start
|
|
30
|
-
expect(sql[0]).toBe("start transaction")
|
|
31
|
-
|
|
32
|
-
// Find the revoke statement for the old database
|
|
33
|
-
const revokeStatement = sql.find(
|
|
34
|
-
(statement) =>
|
|
35
|
-
statement.includes("revoke connect on database") && statement.includes("olddb")
|
|
36
|
-
)
|
|
37
|
-
expect(revokeStatement).toBeDefined()
|
|
38
|
-
|
|
39
|
-
// Check for grant statement for the new database
|
|
40
|
-
const grantStatement = sql.find(
|
|
41
|
-
(statement) =>
|
|
42
|
-
statement.includes("grant connect on database") && statement.includes("newdb")
|
|
43
|
-
)
|
|
44
|
-
expect(grantStatement).toBeDefined()
|
|
45
|
-
|
|
46
|
-
// Check for transaction commit
|
|
47
|
-
expect(sql[sql.length - 1]).toBe("commit")
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
describe("IAM Authentication", () => {
|
|
51
|
-
it("should create role with IAM authentication", async () => {
|
|
52
|
-
const props = {
|
|
53
|
-
EnableIamAuth: true,
|
|
54
|
-
DatabaseName: "testdb",
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const sql = await engine.createRole("iamrole", props)
|
|
58
|
-
|
|
59
|
-
expect(Array.isArray(sql)).toBe(true)
|
|
60
|
-
expect(sql[0]).toBe("start transaction")
|
|
61
|
-
expect(sql).toContain("create role iamrole with login")
|
|
62
|
-
expect(sql).toContain("grant rds_iam to iamrole")
|
|
63
|
-
expect(sql[sql.length - 1]).toBe("commit")
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it("should create role without IAM authentication", async () => {
|
|
67
|
-
jest.spyOn(engine as any, "getPassword").mockResolvedValue("test-password")
|
|
68
|
-
|
|
69
|
-
const props = {
|
|
70
|
-
EnableIamAuth: false,
|
|
71
|
-
PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
|
|
72
|
-
DatabaseName: "testdb",
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const sql = await engine.createRole("passwordrole", props)
|
|
76
|
-
|
|
77
|
-
expect(Array.isArray(sql)).toBe(true)
|
|
78
|
-
expect(sql[0]).toBe("start transaction")
|
|
79
|
-
expect(sql).toContain(
|
|
80
|
-
"create role passwordrole with login password 'test-password'"
|
|
81
|
-
)
|
|
82
|
-
expect(sql[sql.length - 1]).toBe("commit")
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it("should switch from password to IAM authentication", async () => {
|
|
86
|
-
const oldProps = {
|
|
87
|
-
EnableIamAuth: false,
|
|
88
|
-
PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const newProps = {
|
|
92
|
-
EnableIamAuth: true,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const sql = await engine.updateRole(
|
|
96
|
-
"switchrole",
|
|
97
|
-
"switchrole",
|
|
98
|
-
newProps,
|
|
99
|
-
oldProps
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
expect(Array.isArray(sql)).toBe(true)
|
|
103
|
-
expect(sql[0]).toBe("start transaction")
|
|
104
|
-
expect(sql).toContain("grant rds_iam to switchrole")
|
|
105
|
-
expect(sql[sql.length - 1]).toBe("commit")
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it("should switch from IAM to password authentication", async () => {
|
|
109
|
-
jest.spyOn(engine as any, "getPassword").mockResolvedValue("new-password")
|
|
110
|
-
|
|
111
|
-
const oldProps = {
|
|
112
|
-
EnableIamAuth: true,
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const newProps = {
|
|
116
|
-
EnableIamAuth: false,
|
|
117
|
-
PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const sql = await engine.updateRole(
|
|
121
|
-
"switchrole",
|
|
122
|
-
"switchrole",
|
|
123
|
-
newProps,
|
|
124
|
-
oldProps
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
expect(Array.isArray(sql)).toBe(true)
|
|
128
|
-
expect(sql[0]).toBe("start transaction")
|
|
129
|
-
expect(sql).toContain("revoke rds_iam from switchrole")
|
|
130
|
-
expect(sql).toContain("alter role switchrole with password 'new-password'")
|
|
131
|
-
expect(sql[sql.length - 1]).toBe("commit")
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// Add basic tests for other PostgreSQL functionality
|
|
137
|
-
describe("Database", () => {
|
|
138
|
-
it("should generate correct SQL for creating a database", () => {
|
|
139
|
-
const sql = engine.createDatabase("testdb", {})
|
|
140
|
-
expect(sql).toContain("create database testdb")
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
describe("SQL", () => {
|
|
145
|
-
it("should pass through SQL statements", () => {
|
|
146
|
-
const statement = "SELECT * FROM users"
|
|
147
|
-
const sql = engine.createSql("test", { Statement: statement })
|
|
148
|
-
expect(sql).toBe(statement)
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
})
|