cdk-rds-sql 6.1.1 → 6.1.3
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 +3 -3
- package/.pre-commit-config.yaml +1 -1
- package/CLAUDE.md +123 -0
- package/lambda/engine.abstract.ts +30 -12
- package/lambda/engine.factory.ts +1 -1
- package/lambda/engine.mysql.test.ts +35 -10
- package/lambda/engine.mysql.ts +36 -22
- package/lambda/engine.postgresql.test.ts +21 -9
- package/lambda/engine.postgresql.ts +36 -18
- package/lambda/handler.mysql.test.ts +55 -24
- package/lambda/handler.postgresql.test.ts +57 -10
- package/lambda/handler.test.ts +164 -0
- package/lambda/handler.ts +81 -14
- package/lambda/types.ts +29 -14
- package/lambda/util.ts +2 -2
- package/lib/database.js +1 -1
- package/lib/handler/handler.js +11209 -11157
- package/lib/provider.js +5 -4
- package/lib/role.custom-resource.js +2 -2
- package/lib/role.js +1 -1
- package/lib/schema.js +1 -1
- package/lib/sql.js +1 -1
- package/package.json +5 -5
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
2
|
import { createConnection } from "mysql2/promise"
|
|
3
3
|
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
4
|
-
import { createRequest, updateRequest, deleteRequest } from "./util"
|
|
5
4
|
import { handler } from "./handler"
|
|
5
|
+
import { createRequest, updateRequest, deleteRequest } from "./util"
|
|
6
6
|
|
|
7
7
|
jest.mock("@aws-sdk/client-secrets-manager")
|
|
8
8
|
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
9
9
|
typeof SecretsManagerClient
|
|
10
10
|
>
|
|
11
|
-
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
|
|
12
|
-
return {
|
|
13
|
-
SecretString: JSON.stringify({
|
|
14
|
-
host: mysqlHost,
|
|
15
|
-
port: mysqlPort,
|
|
16
|
-
username: DB_MASTER_USERNAME,
|
|
17
|
-
password: DB_MASTER_PASSWORD,
|
|
18
|
-
dbname: DB_DEFAULT_DB,
|
|
19
|
-
engine: "mysql",
|
|
20
|
-
dbClusterIdentifier: "dummy",
|
|
21
|
-
}),
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
11
|
|
|
25
12
|
const DB_PORT = 3306
|
|
26
13
|
const DB_MASTER_USERNAME = "root"
|
|
@@ -31,7 +18,7 @@ let mysqlContainer: StartedTestContainer
|
|
|
31
18
|
let mysqlHost: string
|
|
32
19
|
let mysqlPort: number
|
|
33
20
|
|
|
34
|
-
|
|
21
|
+
beforeAll(async () => {
|
|
35
22
|
process.env.LOGGER = "true"
|
|
36
23
|
process.env.SSL = "false"
|
|
37
24
|
process.env.CONNECTION_TIMEOUT = "5000"
|
|
@@ -45,26 +32,70 @@ beforeEach(async () => {
|
|
|
45
32
|
.start()
|
|
46
33
|
mysqlHost = mysqlContainer.getHost()
|
|
47
34
|
mysqlPort = mysqlContainer.getMappedPort(DB_PORT)
|
|
48
|
-
}, 30000)
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
// Set up the mock after we have the host and port values
|
|
37
|
+
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
|
|
38
|
+
return {
|
|
39
|
+
SecretString: JSON.stringify({
|
|
40
|
+
host: mysqlHost,
|
|
41
|
+
port: mysqlPort,
|
|
42
|
+
username: DB_MASTER_USERNAME,
|
|
43
|
+
password: DB_MASTER_PASSWORD,
|
|
44
|
+
dbname: DB_DEFAULT_DB,
|
|
45
|
+
engine: "mysql",
|
|
46
|
+
dbClusterIdentifier: "dummy",
|
|
47
|
+
}),
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}, 60000)
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
52
53
|
if (mysqlContainer) {
|
|
53
54
|
await mysqlContainer.stop()
|
|
54
55
|
}
|
|
55
56
|
})
|
|
56
57
|
|
|
58
|
+
beforeEach(async () => {
|
|
59
|
+
jest.clearAllMocks()
|
|
60
|
+
|
|
61
|
+
// Clean up databases and users created by tests
|
|
62
|
+
const connection = await newConnection()
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Drop all databases except system ones and default
|
|
66
|
+
const [databases] = await connection.query(
|
|
67
|
+
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', ?)",
|
|
68
|
+
[DB_DEFAULT_DB]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for (const db of databases) {
|
|
72
|
+
await connection.query(`DROP DATABASE IF EXISTS \`${db.SCHEMA_NAME}\``)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Drop all users except system ones
|
|
76
|
+
const [users] = await connection.query(
|
|
77
|
+
"SELECT User FROM mysql.user WHERE User NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema', 'root')"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
for (const user of users) {
|
|
81
|
+
await connection.query(`DROP USER IF EXISTS '${user.User}'@'%'`)
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
await connection.end()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
57
88
|
// Helper functions for MySQL tests
|
|
58
89
|
async function databaseExists(connection: any, dbName: string): Promise<boolean> {
|
|
59
90
|
const [rows] = await connection.query(
|
|
60
|
-
|
|
91
|
+
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
|
|
61
92
|
[dbName]
|
|
62
93
|
)
|
|
63
94
|
return rows.length > 0
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
async function userExists(connection: any, username: string): Promise<boolean> {
|
|
67
|
-
const [rows] = await connection.query(
|
|
98
|
+
const [rows] = await connection.query("SELECT User FROM mysql.user WHERE User = ?", [
|
|
68
99
|
username,
|
|
69
100
|
])
|
|
70
101
|
return rows.length > 0
|
|
@@ -76,7 +107,7 @@ async function tableExists(
|
|
|
76
107
|
tableName: string
|
|
77
108
|
): Promise<boolean> {
|
|
78
109
|
const [rows] = await connection.query(
|
|
79
|
-
|
|
110
|
+
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
|
|
80
111
|
[dbName, tableName]
|
|
81
112
|
)
|
|
82
113
|
return rows.length > 0
|
|
@@ -103,7 +134,7 @@ test("database", async () => {
|
|
|
103
134
|
const newDatabaseName = "mydb2"
|
|
104
135
|
const create = createRequest("database", oldDatabaseName)
|
|
105
136
|
await handler(create)
|
|
106
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
|
|
137
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
107
138
|
const connection = await newConnection()
|
|
108
139
|
try {
|
|
109
140
|
expect(await databaseExists(connection, oldDatabaseName)).toEqual(true)
|
|
@@ -188,7 +219,7 @@ describe("User creation", () => {
|
|
|
188
219
|
DatabaseName: DB_DEFAULT_DB,
|
|
189
220
|
})
|
|
190
221
|
await handler(create)
|
|
191
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
222
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
192
223
|
|
|
193
224
|
const connection = await newConnection()
|
|
194
225
|
try {
|
|
@@ -227,7 +258,7 @@ describe("User creation", () => {
|
|
|
227
258
|
DatabaseName: DB_DEFAULT_DB,
|
|
228
259
|
})
|
|
229
260
|
await handler(create)
|
|
230
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
261
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
231
262
|
|
|
232
263
|
const connection = await newConnection()
|
|
233
264
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
2
|
import { Client, ClientConfig } from "pg"
|
|
3
3
|
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
4
|
+
import { handler } from "./handler"
|
|
4
5
|
import {
|
|
5
6
|
createRequest,
|
|
6
7
|
updateRequest,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
rowCount,
|
|
13
14
|
roleGrantedForSchema,
|
|
14
15
|
} from "./util"
|
|
15
|
-
import { handler } from "./handler"
|
|
16
16
|
|
|
17
17
|
jest.mock("@aws-sdk/client-secrets-manager")
|
|
18
18
|
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
@@ -41,7 +41,7 @@ let pgContainer: StartedTestContainer
|
|
|
41
41
|
let pgHost: string
|
|
42
42
|
let pgPort: number
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
beforeAll(async () => {
|
|
45
45
|
process.env.LOGGER = "true"
|
|
46
46
|
process.env.SSL = "false"
|
|
47
47
|
process.env.CONNECTION_TIMEOUT = "5000"
|
|
@@ -55,15 +55,62 @@ beforeEach(async () => {
|
|
|
55
55
|
.start()
|
|
56
56
|
pgHost = pgContainer.getHost()
|
|
57
57
|
pgPort = pgContainer.getMappedPort(DB_PORT)
|
|
58
|
-
},
|
|
58
|
+
}, 60000)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
jest.clearAllMocks()
|
|
60
|
+
afterAll(async () => {
|
|
62
61
|
if (pgContainer) {
|
|
63
62
|
await pgContainer.stop()
|
|
64
63
|
}
|
|
65
64
|
})
|
|
66
65
|
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
jest.clearAllMocks()
|
|
68
|
+
|
|
69
|
+
// Clean up databases, schemas, and roles created by tests
|
|
70
|
+
const client = new Client({
|
|
71
|
+
host: pgHost,
|
|
72
|
+
port: pgPort,
|
|
73
|
+
database: DB_DEFAULT_DB,
|
|
74
|
+
user: DB_MASTER_USERNAME,
|
|
75
|
+
password: DB_MASTER_PASSWORD,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await client.connect()
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Drop all databases except system ones and default
|
|
82
|
+
const databases = await client.query(
|
|
83
|
+
"SELECT datname FROM pg_database WHERE datistemplate = false AND datname != $1",
|
|
84
|
+
[DB_DEFAULT_DB]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for (const db of databases.rows) {
|
|
88
|
+
await client.query(`DROP DATABASE IF EXISTS "${db.datname}"`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Drop all schemas except system ones
|
|
92
|
+
const schemas = await client.query(
|
|
93
|
+
"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'public')"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
for (const schema of schemas.rows) {
|
|
97
|
+
await client.query(`DROP SCHEMA IF EXISTS "${schema.schema_name}" CASCADE`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Drop all roles except system ones
|
|
101
|
+
const roles = await client.query(
|
|
102
|
+
"SELECT rolname FROM pg_roles WHERE rolname NOT LIKE 'pg_%' AND rolname != $1",
|
|
103
|
+
[DB_MASTER_USERNAME]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
for (const role of roles.rows) {
|
|
107
|
+
await client.query(`DROP ROLE IF EXISTS "${role.rolname}"`)
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
await client.end()
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
67
114
|
//jest.setTimeout(ms("15s"))
|
|
68
115
|
|
|
69
116
|
test("schema", async () => {
|
|
@@ -71,7 +118,7 @@ test("schema", async () => {
|
|
|
71
118
|
const newSchemaName = "test2"
|
|
72
119
|
const create = createRequest("schema", oldSchemaName)
|
|
73
120
|
await handler(create)
|
|
74
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
|
|
121
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
75
122
|
|
|
76
123
|
const client = await newClient()
|
|
77
124
|
try {
|
|
@@ -128,7 +175,7 @@ test("role with existing database", async () => {
|
|
|
128
175
|
DatabaseName: "postgres",
|
|
129
176
|
})
|
|
130
177
|
await handler(create)
|
|
131
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
178
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
132
179
|
const client = await newClient()
|
|
133
180
|
try {
|
|
134
181
|
expect(await roleExists(client, oldRoleName)).toEqual(true)
|
|
@@ -165,7 +212,7 @@ test("role without database", async () => {
|
|
|
165
212
|
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
166
213
|
})
|
|
167
214
|
await handler(create)
|
|
168
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
215
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
169
216
|
const client = await newClient()
|
|
170
217
|
try {
|
|
171
218
|
expect(await roleExists(client, roleName)).toEqual(true)
|
|
@@ -180,7 +227,7 @@ test("change role password", async () => {
|
|
|
180
227
|
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
181
228
|
})
|
|
182
229
|
await handler(create)
|
|
183
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
|
|
230
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
184
231
|
const client = await newClient()
|
|
185
232
|
try {
|
|
186
233
|
expect(await roleExists(client, roleName)).toEqual(true)
|
|
@@ -198,7 +245,7 @@ test("database", async () => {
|
|
|
198
245
|
const newDatabaseName = "mydb2"
|
|
199
246
|
const create = createRequest("database", oldDatabaseName)
|
|
200
247
|
await handler(create)
|
|
201
|
-
expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
|
|
248
|
+
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
202
249
|
const client = await newClient()
|
|
203
250
|
try {
|
|
204
251
|
expect(await databaseExists(client, oldDatabaseName)).toEqual(true)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
|
+
import { MysqlEngine } from "./engine.mysql"
|
|
3
|
+
import { PostgresqlEngine } from "./engine.postgresql"
|
|
4
|
+
import { handler } from "./handler"
|
|
5
|
+
import { createRequest } from "./util"
|
|
6
|
+
|
|
7
|
+
// Mock the secrets manager client
|
|
8
|
+
jest.mock("@aws-sdk/client-secrets-manager")
|
|
9
|
+
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
10
|
+
typeof SecretsManagerClient
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
// Mock the database engines
|
|
14
|
+
jest.mock("./engine.mysql")
|
|
15
|
+
jest.mock("./engine.postgresql")
|
|
16
|
+
|
|
17
|
+
const MockedMysqlEngine = MysqlEngine as jest.MockedClass<typeof MysqlEngine>
|
|
18
|
+
const MockedPostgresqlEngine = PostgresqlEngine as jest.MockedClass<
|
|
19
|
+
typeof PostgresqlEngine
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
describe("Handler CloudFormation Property Conversion", () => {
|
|
23
|
+
let mockMysqlEngine: jest.Mocked<MysqlEngine>
|
|
24
|
+
let mockPostgresqlEngine: jest.Mocked<PostgresqlEngine>
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks()
|
|
28
|
+
|
|
29
|
+
// Mock the engine instances
|
|
30
|
+
mockMysqlEngine = {
|
|
31
|
+
createRole: jest.fn(),
|
|
32
|
+
executeSQL: jest.fn(),
|
|
33
|
+
} as any
|
|
34
|
+
|
|
35
|
+
mockPostgresqlEngine = {
|
|
36
|
+
createRole: jest.fn(),
|
|
37
|
+
executeSQL: jest.fn(),
|
|
38
|
+
} as any
|
|
39
|
+
|
|
40
|
+
MockedMysqlEngine.mockImplementation(() => mockMysqlEngine)
|
|
41
|
+
MockedPostgresqlEngine.mockImplementation(() => mockPostgresqlEngine)
|
|
42
|
+
|
|
43
|
+
// Mock secrets manager to return MySQL engine
|
|
44
|
+
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
|
|
45
|
+
return Promise.resolve({
|
|
46
|
+
SecretString: JSON.stringify({
|
|
47
|
+
host: "localhost",
|
|
48
|
+
port: 3306,
|
|
49
|
+
username: "root",
|
|
50
|
+
password: "password",
|
|
51
|
+
dbname: "test",
|
|
52
|
+
engine: "mysql",
|
|
53
|
+
}),
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("EnableIamAuth property conversion", () => {
|
|
59
|
+
it("should convert string 'false' to boolean false", async () => {
|
|
60
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
61
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
62
|
+
|
|
63
|
+
// Create a request with EnableIamAuth as string 'false' (simulating CloudFormation)
|
|
64
|
+
const request = createRequest("role", "testuser", {
|
|
65
|
+
EnableIamAuth: "false",
|
|
66
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await handler(request)
|
|
70
|
+
|
|
71
|
+
// Verify that createRole was called with boolean false, not string 'false'
|
|
72
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
73
|
+
"testuser",
|
|
74
|
+
expect.objectContaining({
|
|
75
|
+
EnableIamAuth: false, // Should be converted to boolean false
|
|
76
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should convert string 'true' to boolean true", async () => {
|
|
82
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
83
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
84
|
+
|
|
85
|
+
// Create a request with EnableIamAuth as string 'true' (simulating CloudFormation)
|
|
86
|
+
const request = createRequest("role", "testuser", {
|
|
87
|
+
EnableIamAuth: "true", // CloudFormation passes boolean as string
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await handler(request)
|
|
91
|
+
|
|
92
|
+
// Verify that createRole was called with boolean true, not string 'true'
|
|
93
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
94
|
+
"testuser",
|
|
95
|
+
expect.objectContaining({
|
|
96
|
+
EnableIamAuth: true, // Should be converted to boolean true
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("should handle number 0 correctly", async () => {
|
|
102
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
103
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
104
|
+
|
|
105
|
+
// Create a request with EnableIamAuth as actual boolean false
|
|
106
|
+
const request = createRequest("role", "testuser", {
|
|
107
|
+
EnableIamAuth: "0",
|
|
108
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await handler(request)
|
|
112
|
+
|
|
113
|
+
// Verify that createRole was called with boolean false
|
|
114
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
115
|
+
"testuser",
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
EnableIamAuth: false,
|
|
118
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("should handle number 1 correctly", async () => {
|
|
124
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
125
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
126
|
+
|
|
127
|
+
// Create a request with EnableIamAuth as actual boolean true
|
|
128
|
+
const request = createRequest("role", "testuser", {
|
|
129
|
+
EnableIamAuth: "1",
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await handler(request)
|
|
133
|
+
|
|
134
|
+
// Verify that createRole was called with boolean true
|
|
135
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
136
|
+
"testuser",
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
EnableIamAuth: true,
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("should handle undefined EnableIamAuth correctly", async () => {
|
|
144
|
+
mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
|
|
145
|
+
mockMysqlEngine.executeSQL.mockResolvedValue({})
|
|
146
|
+
|
|
147
|
+
// Create a request without EnableIamAuth property
|
|
148
|
+
const request = createRequest("role", "testuser", {
|
|
149
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
await handler(request)
|
|
153
|
+
|
|
154
|
+
// Verify that createRole was called without EnableIamAuth
|
|
155
|
+
expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
|
|
156
|
+
"testuser",
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
159
|
+
EnableIamAuth: false,
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
package/lambda/handler.ts
CHANGED
|
@@ -15,10 +15,17 @@ import {
|
|
|
15
15
|
CloudFormationCustomResourceDeleteEvent,
|
|
16
16
|
} from "aws-lambda"
|
|
17
17
|
import { backOff } from "exponential-backoff"
|
|
18
|
-
import { EngineFactory } from "./engine.factory"
|
|
19
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
|
+
} from "./types"
|
|
20
28
|
import { RdsSqlResource } from "../src/enum"
|
|
21
|
-
import { ResourceProperties, CustomResourceResponse } from "./types"
|
|
22
29
|
|
|
23
30
|
const maxAttempts = 20
|
|
24
31
|
|
|
@@ -29,6 +36,40 @@ const log =
|
|
|
29
36
|
|
|
30
37
|
const secrets_client = new SecretsManagerClient({})
|
|
31
38
|
|
|
39
|
+
// Helper functions to extract engine properties from CloudFormation properties
|
|
40
|
+
// These functions throw if the resource type doesn't match the expected type
|
|
41
|
+
function toDatabaseEngineProps(props: ResourceProperties): EngineDatabaseProperties {
|
|
42
|
+
if (props.Resource !== RdsSqlResource.DATABASE) {
|
|
43
|
+
throw new Error(`Expected DATABASE resource, got ${props.Resource}`)
|
|
44
|
+
}
|
|
45
|
+
return props
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toRoleEngineProps(props: ResourceProperties): EngineRoleProperties {
|
|
49
|
+
if (props.Resource !== RdsSqlResource.ROLE) {
|
|
50
|
+
throw new Error(`Expected ROLE resource, got ${props.Resource}`)
|
|
51
|
+
}
|
|
52
|
+
// Convert string 'true'/'false' to boolean for EnableIamAuth
|
|
53
|
+
return {
|
|
54
|
+
...props,
|
|
55
|
+
EnableIamAuth: props.EnableIamAuth === "true" || props.EnableIamAuth === "1",
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toSchemaEngineProps(props: ResourceProperties): EngineSchemaProperties {
|
|
60
|
+
if (props.Resource !== RdsSqlResource.SCHEMA) {
|
|
61
|
+
throw new Error(`Expected SCHEMA resource, got ${props.Resource}`)
|
|
62
|
+
}
|
|
63
|
+
return props
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toSqlEngineProps(props: ResourceProperties): EngineSqlProperties {
|
|
67
|
+
if (props.Resource !== RdsSqlResource.SQL) {
|
|
68
|
+
throw new Error(`Expected SQL resource, got ${props.Resource}`)
|
|
69
|
+
}
|
|
70
|
+
return props
|
|
71
|
+
}
|
|
72
|
+
|
|
32
73
|
export const handler = async (
|
|
33
74
|
event:
|
|
34
75
|
| CloudFormationCustomResourceCreateEvent<ResourceProperties>
|
|
@@ -82,16 +123,25 @@ export const handler = async (
|
|
|
82
123
|
case "Create": {
|
|
83
124
|
switch (resource) {
|
|
84
125
|
case RdsSqlResource.DATABASE:
|
|
85
|
-
sql = dbEngine.createDatabase(
|
|
126
|
+
sql = dbEngine.createDatabase(
|
|
127
|
+
resourceId,
|
|
128
|
+
toDatabaseEngineProps(event.ResourceProperties)
|
|
129
|
+
)
|
|
86
130
|
break
|
|
87
131
|
case RdsSqlResource.ROLE:
|
|
88
|
-
sql = await dbEngine.createRole(
|
|
132
|
+
sql = await dbEngine.createRole(
|
|
133
|
+
resourceId,
|
|
134
|
+
toRoleEngineProps(event.ResourceProperties)
|
|
135
|
+
)
|
|
89
136
|
break
|
|
90
137
|
case RdsSqlResource.SCHEMA:
|
|
91
|
-
sql = dbEngine.createSchema(
|
|
138
|
+
sql = dbEngine.createSchema(
|
|
139
|
+
resourceId,
|
|
140
|
+
toSchemaEngineProps(event.ResourceProperties)
|
|
141
|
+
)
|
|
92
142
|
break
|
|
93
143
|
case RdsSqlResource.SQL:
|
|
94
|
-
sql = dbEngine.createSql(resourceId, event.ResourceProperties)
|
|
144
|
+
sql = dbEngine.createSql(resourceId, toSqlEngineProps(event.ResourceProperties))
|
|
95
145
|
break
|
|
96
146
|
case RdsSqlResource.PARAMETER_PASSWORD:
|
|
97
147
|
await handleParameterPassword(event.ResourceProperties)
|
|
@@ -104,24 +154,35 @@ export const handler = async (
|
|
|
104
154
|
.PhysicalResourceId
|
|
105
155
|
switch (resource) {
|
|
106
156
|
case RdsSqlResource.DATABASE:
|
|
157
|
+
const dbProps = toDatabaseEngineProps(event.ResourceProperties)
|
|
107
158
|
sql = dbEngine.updateDatabase(resourceId, oldResourceId, {
|
|
108
|
-
...
|
|
159
|
+
...dbProps,
|
|
109
160
|
MasterOwner: secretValues.username,
|
|
110
161
|
})
|
|
111
162
|
break
|
|
112
163
|
case RdsSqlResource.ROLE:
|
|
164
|
+
const updateEvent =
|
|
165
|
+
event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
|
|
113
166
|
sql = await dbEngine.updateRole(
|
|
114
167
|
resourceId,
|
|
115
168
|
oldResourceId,
|
|
116
|
-
event.ResourceProperties,
|
|
117
|
-
(
|
|
169
|
+
toRoleEngineProps(event.ResourceProperties),
|
|
170
|
+
toRoleEngineProps(updateEvent.OldResourceProperties)
|
|
118
171
|
)
|
|
119
172
|
break
|
|
120
173
|
case RdsSqlResource.SCHEMA:
|
|
121
|
-
sql = dbEngine.updateSchema(
|
|
174
|
+
sql = dbEngine.updateSchema(
|
|
175
|
+
resourceId,
|
|
176
|
+
oldResourceId,
|
|
177
|
+
toSchemaEngineProps(event.ResourceProperties)
|
|
178
|
+
)
|
|
122
179
|
break
|
|
123
180
|
case RdsSqlResource.SQL:
|
|
124
|
-
sql = dbEngine.updateSql(
|
|
181
|
+
sql = dbEngine.updateSql(
|
|
182
|
+
resourceId,
|
|
183
|
+
oldResourceId,
|
|
184
|
+
toSqlEngineProps(event.ResourceProperties)
|
|
185
|
+
)
|
|
125
186
|
break
|
|
126
187
|
case RdsSqlResource.PARAMETER_PASSWORD:
|
|
127
188
|
await handleParameterPassword(event.ResourceProperties)
|
|
@@ -136,13 +197,19 @@ export const handler = async (
|
|
|
136
197
|
sql = dbEngine.deleteDatabase(resourceId, secretValues.username)
|
|
137
198
|
break
|
|
138
199
|
case RdsSqlResource.ROLE:
|
|
139
|
-
sql = dbEngine.deleteRole(
|
|
200
|
+
sql = dbEngine.deleteRole(
|
|
201
|
+
resourceId,
|
|
202
|
+
toRoleEngineProps(event.ResourceProperties)
|
|
203
|
+
)
|
|
140
204
|
break
|
|
141
205
|
case RdsSqlResource.SCHEMA:
|
|
142
|
-
sql = dbEngine.deleteSchema(
|
|
206
|
+
sql = dbEngine.deleteSchema(
|
|
207
|
+
resourceId,
|
|
208
|
+
toSchemaEngineProps(event.ResourceProperties)
|
|
209
|
+
)
|
|
143
210
|
break
|
|
144
211
|
case RdsSqlResource.SQL:
|
|
145
|
-
sql = dbEngine.deleteSql(resourceId, event.ResourceProperties)
|
|
212
|
+
sql = dbEngine.deleteSql(resourceId, toSqlEngineProps(event.ResourceProperties))
|
|
146
213
|
break
|
|
147
214
|
case RdsSqlResource.PARAMETER_PASSWORD:
|
|
148
215
|
await deleteParameterPassword(event.ResourceProperties)
|
package/lambda/types.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import { CloudFormationCustomResourceResourcePropertiesCommon } from "aws-lambda"
|
|
2
2
|
import { RdsSqlResource } from "../src/enum"
|
|
3
3
|
|
|
4
|
-
//
|
|
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
|
+
// Common CloudFormation properties shared by all resources
|
|
5
28
|
export interface CommonProperties
|
|
6
29
|
extends CloudFormationCustomResourceResourcePropertiesCommon {
|
|
7
30
|
readonly Resource: RdsSqlResource
|
|
@@ -9,32 +32,24 @@ export interface CommonProperties
|
|
|
9
32
|
readonly SecretArn: string
|
|
10
33
|
}
|
|
11
34
|
|
|
12
|
-
//
|
|
13
|
-
export interface DatabaseProperties extends CommonProperties {
|
|
35
|
+
// CloudFormation-specific properties (engine properties + CloudFormation metadata)
|
|
36
|
+
export interface DatabaseProperties extends CommonProperties, EngineDatabaseProperties {
|
|
14
37
|
readonly Resource: RdsSqlResource.DATABASE
|
|
15
|
-
readonly Owner?: string
|
|
16
38
|
}
|
|
17
39
|
|
|
18
|
-
// Role specific properties
|
|
19
40
|
export interface RoleProperties extends CommonProperties {
|
|
20
41
|
readonly Resource: RdsSqlResource.ROLE
|
|
21
42
|
readonly PasswordArn?: string
|
|
22
43
|
readonly DatabaseName?: string
|
|
44
|
+
readonly EnableIamAuth?: string // CloudFormation passes boolean as string
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
|
|
26
|
-
export interface SchemaProperties extends CommonProperties {
|
|
47
|
+
export interface SchemaProperties extends CommonProperties, EngineSchemaProperties {
|
|
27
48
|
readonly Resource: RdsSqlResource.SCHEMA
|
|
28
|
-
readonly DatabaseName?: string
|
|
29
|
-
readonly RoleName?: string
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
export interface SqlProperties extends CommonProperties {
|
|
51
|
+
export interface SqlProperties extends CommonProperties, EngineSqlProperties {
|
|
34
52
|
readonly Resource: RdsSqlResource.SQL
|
|
35
|
-
readonly DatabaseName?: string
|
|
36
|
-
readonly Statement?: string
|
|
37
|
-
readonly Rollback?: string
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
// Parameter password specific properties
|
package/lambda/util.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface ResourcePropertiesInput {
|
|
|
25
25
|
export const createRequest = (
|
|
26
26
|
resource: string,
|
|
27
27
|
resourceId: string,
|
|
28
|
-
props?:
|
|
28
|
+
props?: Partial<ResourceProperties>
|
|
29
29
|
): CloudFormationCustomResourceCreateEvent<ResourceProperties> => {
|
|
30
30
|
return {
|
|
31
31
|
ServiceToken: "",
|
|
@@ -41,7 +41,7 @@ export const createRequest = (
|
|
|
41
41
|
SecretArn:
|
|
42
42
|
props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
43
43
|
...props,
|
|
44
|
-
}
|
|
44
|
+
},
|
|
45
45
|
RequestType: "Create",
|
|
46
46
|
}
|
|
47
47
|
}
|