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/.jsii +8 -8
- 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.js +1 -1
- 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
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs"
|
|
2
|
-
import { ConnectionOptions } from "tls"
|
|
3
|
-
import { format as pgFormat } from "node-pg-format"
|
|
4
|
-
import { Client, ClientConfig } from "pg"
|
|
5
|
-
import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract"
|
|
6
|
-
import {
|
|
7
|
-
EngineDatabaseProperties,
|
|
8
|
-
EngineRoleProperties,
|
|
9
|
-
EngineSchemaProperties,
|
|
10
|
-
EngineSqlProperties,
|
|
11
|
-
} from "./types"
|
|
12
|
-
|
|
13
|
-
export class PostgresqlEngine extends AbstractEngine {
|
|
14
|
-
createDatabase(resourceId: string, props: EngineDatabaseProperties): string | string[] {
|
|
15
|
-
const owner = props?.Owner
|
|
16
|
-
if (owner) {
|
|
17
|
-
return [
|
|
18
|
-
pgFormat("create database %I", resourceId),
|
|
19
|
-
pgFormat("alter database %I owner to %I", resourceId, owner),
|
|
20
|
-
]
|
|
21
|
-
} else {
|
|
22
|
-
return pgFormat("create database %I", resourceId)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
updateDatabase(
|
|
27
|
-
resourceId: string,
|
|
28
|
-
oldResourceId: string,
|
|
29
|
-
props: EngineDatabaseProperties
|
|
30
|
-
): string[] {
|
|
31
|
-
const statements: string[] = []
|
|
32
|
-
if (resourceId !== oldResourceId) {
|
|
33
|
-
if (props?.MasterOwner) {
|
|
34
|
-
statements.push(
|
|
35
|
-
pgFormat("alter database %I owner to %I", oldResourceId, props.MasterOwner)
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
statements.push(
|
|
39
|
-
pgFormat("alter database %I rename to %I", oldResourceId, resourceId)
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
const owner = props?.Owner
|
|
43
|
-
if (owner) {
|
|
44
|
-
statements.push(pgFormat("alter database %I owner to %I", resourceId, props.Owner))
|
|
45
|
-
}
|
|
46
|
-
return statements
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
deleteDatabase(resourceId: string, masterUser: string): string[] {
|
|
50
|
-
return [
|
|
51
|
-
pgFormat(
|
|
52
|
-
"select pg_terminate_backend(pg_stat_activity.pid) from pg_stat_activity where datname = %L",
|
|
53
|
-
resourceId
|
|
54
|
-
),
|
|
55
|
-
pgFormat(
|
|
56
|
-
"DO $$BEGIN\nIF EXISTS (select from pg_database WHERE datname = '%s') THEN alter database %I owner to %I; END IF;\nEND$$;",
|
|
57
|
-
resourceId,
|
|
58
|
-
resourceId,
|
|
59
|
-
masterUser
|
|
60
|
-
),
|
|
61
|
-
pgFormat("drop database if exists %I", resourceId),
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
|
|
66
|
-
const sql = ["start transaction"]
|
|
67
|
-
|
|
68
|
-
if (props?.EnableIamAuth) {
|
|
69
|
-
// Create role for IAM authentication
|
|
70
|
-
sql.push(pgFormat("create role %I with login", resourceId))
|
|
71
|
-
sql.push(pgFormat("grant rds_iam to %I", resourceId))
|
|
72
|
-
} else {
|
|
73
|
-
// Create role with password authentication
|
|
74
|
-
if (!props?.PasswordArn) throw "No PasswordArn provided"
|
|
75
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
76
|
-
if (!password) {
|
|
77
|
-
throw `Cannot parse password from ${props.PasswordArn}`
|
|
78
|
-
}
|
|
79
|
-
sql.push(pgFormat("create role %I with login password %L", resourceId, password))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (props?.DatabaseName) {
|
|
83
|
-
sql.push(
|
|
84
|
-
pgFormat(
|
|
85
|
-
`DO $$
|
|
86
|
-
BEGIN
|
|
87
|
-
IF EXISTS (select from pg_database where datname = '%s' and datistemplate = false) THEN
|
|
88
|
-
grant connect on database %I to %I;
|
|
89
|
-
END IF;
|
|
90
|
-
END$$;`,
|
|
91
|
-
props.DatabaseName,
|
|
92
|
-
props.DatabaseName,
|
|
93
|
-
resourceId
|
|
94
|
-
)
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
sql.push("commit")
|
|
99
|
-
return sql
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async updateRole(
|
|
103
|
-
resourceId: string,
|
|
104
|
-
oldResourceId: string,
|
|
105
|
-
props: EngineRoleProperties,
|
|
106
|
-
oldProps: EngineRoleProperties
|
|
107
|
-
): Promise<string[]> {
|
|
108
|
-
const sql = ["start transaction"]
|
|
109
|
-
|
|
110
|
-
if (oldResourceId !== resourceId) {
|
|
111
|
-
sql.push(pgFormat("alter role %I rename to %I", oldResourceId, resourceId))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Handle authentication method changes
|
|
115
|
-
if (props?.EnableIamAuth && !oldProps?.EnableIamAuth) {
|
|
116
|
-
// Switching from password to IAM auth
|
|
117
|
-
sql.push(pgFormat("grant rds_iam to %I", resourceId))
|
|
118
|
-
} else if (!props?.EnableIamAuth && oldProps?.EnableIamAuth) {
|
|
119
|
-
// Switching from IAM to password auth
|
|
120
|
-
sql.push(pgFormat("revoke rds_iam from %I", resourceId))
|
|
121
|
-
if (props?.PasswordArn) {
|
|
122
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
123
|
-
if (!password) {
|
|
124
|
-
throw `Cannot parse password from ${props.PasswordArn}`
|
|
125
|
-
}
|
|
126
|
-
sql.push(pgFormat("alter role %I with password %L", resourceId, password))
|
|
127
|
-
}
|
|
128
|
-
} else if (!props?.EnableIamAuth && props?.PasswordArn) {
|
|
129
|
-
// Update password for password auth
|
|
130
|
-
const password = await this.getPassword(props.PasswordArn)
|
|
131
|
-
if (!password) {
|
|
132
|
-
throw `Cannot parse password from ${props.PasswordArn}`
|
|
133
|
-
}
|
|
134
|
-
sql.push(pgFormat("alter role %I with password %L", resourceId, password))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check if database name has changed
|
|
138
|
-
if (
|
|
139
|
-
oldProps?.DatabaseName &&
|
|
140
|
-
props?.DatabaseName &&
|
|
141
|
-
oldProps.DatabaseName !== props.DatabaseName
|
|
142
|
-
) {
|
|
143
|
-
// Revoke from old database
|
|
144
|
-
sql.push(
|
|
145
|
-
pgFormat(
|
|
146
|
-
`DO $$
|
|
147
|
-
BEGIN
|
|
148
|
-
IF EXISTS (select from pg_database where datname = '%s' and datistemplate = false) THEN
|
|
149
|
-
revoke connect on database %I from %I;
|
|
150
|
-
END IF;
|
|
151
|
-
END$$;`,
|
|
152
|
-
oldProps.DatabaseName,
|
|
153
|
-
oldProps.DatabaseName,
|
|
154
|
-
resourceId
|
|
155
|
-
)
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (props?.DatabaseName) {
|
|
160
|
-
sql.push(
|
|
161
|
-
pgFormat(
|
|
162
|
-
`DO $$
|
|
163
|
-
BEGIN
|
|
164
|
-
IF EXISTS (select from pg_database where datname = '%s' and datistemplate = false) THEN
|
|
165
|
-
grant connect on database %I to %I;
|
|
166
|
-
END IF;
|
|
167
|
-
END$$;`,
|
|
168
|
-
props.DatabaseName,
|
|
169
|
-
props.DatabaseName,
|
|
170
|
-
resourceId
|
|
171
|
-
)
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
sql.push("commit")
|
|
176
|
-
return sql
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async deleteRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
|
|
180
|
-
return [
|
|
181
|
-
"start transaction",
|
|
182
|
-
pgFormat(
|
|
183
|
-
`DO $$
|
|
184
|
-
BEGIN
|
|
185
|
-
IF EXISTS (select from pg_catalog.pg_roles WHERE rolname = '%s') AND EXISTS (select from pg_database WHERE datname = '%s') THEN
|
|
186
|
-
revoke all privileges on database %I from %I;
|
|
187
|
-
END IF;
|
|
188
|
-
END$$;`,
|
|
189
|
-
resourceId,
|
|
190
|
-
props?.DatabaseName,
|
|
191
|
-
props?.DatabaseName,
|
|
192
|
-
resourceId
|
|
193
|
-
),
|
|
194
|
-
pgFormat("drop role if exists %I", resourceId),
|
|
195
|
-
"commit",
|
|
196
|
-
]
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
createSchema(resourceId: string, props: EngineSchemaProperties): string[] {
|
|
200
|
-
const sql: string[] = [pgFormat("create schema if not exists %I", resourceId)]
|
|
201
|
-
if (props?.RoleName) {
|
|
202
|
-
this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
203
|
-
sql.push(stmt)
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
return sql
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
updateSchema(
|
|
210
|
-
resourceId: string,
|
|
211
|
-
oldResourceId: string,
|
|
212
|
-
props: EngineSchemaProperties
|
|
213
|
-
): string[] {
|
|
214
|
-
const sql: string[] = []
|
|
215
|
-
if (props?.RoleName) {
|
|
216
|
-
this.revokeRoleFromSchema(oldResourceId, props.RoleName).forEach((stmt) =>
|
|
217
|
-
sql.push(stmt)
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
sql.push(pgFormat("alter schema %I rename to %I", oldResourceId, resourceId))
|
|
221
|
-
if (props?.RoleName) {
|
|
222
|
-
this.grantRoleForSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
223
|
-
sql.push(stmt)
|
|
224
|
-
)
|
|
225
|
-
}
|
|
226
|
-
return sql
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
deleteSchema(resourceId: string, props: EngineSchemaProperties): string[] {
|
|
230
|
-
const sql: string[] = []
|
|
231
|
-
if (props?.RoleName) {
|
|
232
|
-
this.revokeRoleFromSchema(resourceId, props.RoleName).forEach((stmt) =>
|
|
233
|
-
sql.push(stmt)
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
sql.push(pgFormat("drop schema if exists %I cascade", resourceId))
|
|
237
|
-
return sql
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
createSql(_resourceId: string, props: EngineSqlProperties): string {
|
|
241
|
-
return props?.Statement || ""
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
updateSql(
|
|
245
|
-
_resourceId: string,
|
|
246
|
-
_oldResourceId: string,
|
|
247
|
-
props: EngineSqlProperties
|
|
248
|
-
): string {
|
|
249
|
-
return props?.Statement || ""
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
deleteSql(_resourceId: string, props: EngineSqlProperties): string {
|
|
253
|
-
return props?.Rollback || ""
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private grantRoleForSchema(schema: string, roleName: string): string[] {
|
|
257
|
-
return [
|
|
258
|
-
pgFormat("GRANT USAGE ON SCHEMA %I TO %I", schema, roleName),
|
|
259
|
-
pgFormat("GRANT CREATE ON SCHEMA %I TO %I", schema, roleName),
|
|
260
|
-
]
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
private revokeRoleFromSchema(schema: string, roleName: string): string[] {
|
|
264
|
-
return [
|
|
265
|
-
pgFormat("REVOKE CREATE ON SCHEMA %I FROM %I", schema, roleName),
|
|
266
|
-
pgFormat("REVOKE ALL ON SCHEMA %I FROM %I", schema, roleName),
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
createIamGrant(_roleName: string, _iamArn: string): string | string[] {
|
|
271
|
-
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
updateIamGrant(
|
|
275
|
-
_roleName: string,
|
|
276
|
-
_oldRoleName: string,
|
|
277
|
-
_iamArn: string,
|
|
278
|
-
_oldIamArn: string
|
|
279
|
-
): string | string[] {
|
|
280
|
-
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
deleteIamGrant(_roleName: string, _iamArn: string): string | string[] {
|
|
284
|
-
throw new Error("IAM grants are only supported with DSQL clusters")
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
|
|
288
|
-
const isSslEnabled = process.env.SSL ? JSON.parse(process.env.SSL) : true
|
|
289
|
-
const ssl: ConnectionOptions | false = isSslEnabled
|
|
290
|
-
? {
|
|
291
|
-
ca: fs.readFileSync(`${process.env.LAMBDA_TASK_ROOT}/global-bundle.pem`),
|
|
292
|
-
rejectUnauthorized: true,
|
|
293
|
-
}
|
|
294
|
-
: false
|
|
295
|
-
|
|
296
|
-
const params: ClientConfig = {
|
|
297
|
-
host: config.host,
|
|
298
|
-
port: config.port,
|
|
299
|
-
user: config.user,
|
|
300
|
-
password: config.password,
|
|
301
|
-
database: config.database,
|
|
302
|
-
connectionTimeoutMillis: Number(process.env.CONNECTION_TIMEOUT) ?? 30000,
|
|
303
|
-
ssl,
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
this.log(
|
|
307
|
-
`Connecting to PostgreSQL host ${params.host}:${params.port}${
|
|
308
|
-
ssl ? " using a secure connection" : ""
|
|
309
|
-
}, database ${params.database} as ${params.user}`
|
|
310
|
-
)
|
|
311
|
-
this.log("Executing SQL", sql)
|
|
312
|
-
|
|
313
|
-
const pg_client = new Client(params)
|
|
314
|
-
await pg_client.connect()
|
|
315
|
-
try {
|
|
316
|
-
if (typeof sql === "string") {
|
|
317
|
-
await pg_client.query(sql)
|
|
318
|
-
} else if (sql) {
|
|
319
|
-
await Promise.all(
|
|
320
|
-
sql.map((statement) => {
|
|
321
|
-
return pg_client.query(statement)
|
|
322
|
-
})
|
|
323
|
-
)
|
|
324
|
-
}
|
|
325
|
-
} finally {
|
|
326
|
-
await pg_client.end()
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
|
|
2
|
-
import { createConnection } from "mysql2/promise"
|
|
3
|
-
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
4
|
-
import { handler } from "./handler"
|
|
5
|
-
import { createRequest, updateRequest, deleteRequest } from "./util"
|
|
6
|
-
|
|
7
|
-
jest.mock("@aws-sdk/client-secrets-manager")
|
|
8
|
-
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
|
|
9
|
-
typeof SecretsManagerClient
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
const DB_PORT = 3306
|
|
13
|
-
const DB_MASTER_USERNAME = "root"
|
|
14
|
-
const DB_MASTER_PASSWORD = "masterpwd"
|
|
15
|
-
const DB_DEFAULT_DB = "dummy"
|
|
16
|
-
|
|
17
|
-
let mysqlContainer: StartedTestContainer
|
|
18
|
-
let mysqlHost: string
|
|
19
|
-
let mysqlPort: number
|
|
20
|
-
|
|
21
|
-
beforeAll(async () => {
|
|
22
|
-
process.env.LOGGER = "true"
|
|
23
|
-
process.env.SSL = "false"
|
|
24
|
-
process.env.CONNECTION_TIMEOUT = "5000"
|
|
25
|
-
mysqlContainer = await new GenericContainer("mysql:8")
|
|
26
|
-
.withExposedPorts(DB_PORT)
|
|
27
|
-
.withEnvironment({
|
|
28
|
-
MYSQL_ROOT_PASSWORD: DB_MASTER_PASSWORD,
|
|
29
|
-
MYSQL_DATABASE: DB_DEFAULT_DB,
|
|
30
|
-
MYSQL_INITDB_SKIP_TZINFO: "true",
|
|
31
|
-
})
|
|
32
|
-
.start()
|
|
33
|
-
mysqlHost = mysqlContainer.getHost()
|
|
34
|
-
mysqlPort = mysqlContainer.getMappedPort(DB_PORT)
|
|
35
|
-
|
|
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 () => {
|
|
53
|
-
if (mysqlContainer) {
|
|
54
|
-
await mysqlContainer.stop()
|
|
55
|
-
}
|
|
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
|
-
|
|
88
|
-
// Helper functions for MySQL tests
|
|
89
|
-
async function databaseExists(connection: any, dbName: string): Promise<boolean> {
|
|
90
|
-
const [rows] = await connection.query(
|
|
91
|
-
"SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
|
|
92
|
-
[dbName]
|
|
93
|
-
)
|
|
94
|
-
return rows.length > 0
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function userExists(connection: any, username: string): Promise<boolean> {
|
|
98
|
-
const [rows] = await connection.query("SELECT User FROM mysql.user WHERE User = ?", [
|
|
99
|
-
username,
|
|
100
|
-
])
|
|
101
|
-
return rows.length > 0
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function tableExists(
|
|
105
|
-
connection: any,
|
|
106
|
-
dbName: string,
|
|
107
|
-
tableName: string
|
|
108
|
-
): Promise<boolean> {
|
|
109
|
-
const [rows] = await connection.query(
|
|
110
|
-
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
|
|
111
|
-
[dbName, tableName]
|
|
112
|
-
)
|
|
113
|
-
return rows.length > 0
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function rowCount(connection: any, tableName: string): Promise<number> {
|
|
117
|
-
const [rows] = await connection.query(`SELECT COUNT(*) as count FROM ${tableName}`)
|
|
118
|
-
return rows[0].count
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function newConnection(config?: any): Promise<any> {
|
|
122
|
-
const conn = await createConnection({
|
|
123
|
-
host: mysqlHost,
|
|
124
|
-
port: mysqlPort,
|
|
125
|
-
database: config && config.database ? config.database : DB_DEFAULT_DB,
|
|
126
|
-
user: config && config.user ? config.user : DB_MASTER_USERNAME,
|
|
127
|
-
password: config && config.password ? config.password : DB_MASTER_PASSWORD,
|
|
128
|
-
})
|
|
129
|
-
return conn
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
test("database", async () => {
|
|
133
|
-
const oldDatabaseName = "mydb"
|
|
134
|
-
const newDatabaseName = "mydb2"
|
|
135
|
-
const create = createRequest("database", oldDatabaseName)
|
|
136
|
-
await handler(create)
|
|
137
|
-
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
|
|
138
|
-
const connection = await newConnection()
|
|
139
|
-
try {
|
|
140
|
-
expect(await databaseExists(connection, oldDatabaseName)).toEqual(true)
|
|
141
|
-
|
|
142
|
-
const update = updateRequest("database", oldDatabaseName, newDatabaseName)
|
|
143
|
-
await expect(handler(update)).rejects.toThrow(
|
|
144
|
-
"Renaming database is not supported in MySQL."
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
const remove = deleteRequest("database", oldDatabaseName)
|
|
148
|
-
await handler(remove)
|
|
149
|
-
expect(await databaseExists(connection, newDatabaseName)).toEqual(false)
|
|
150
|
-
} finally {
|
|
151
|
-
await connection.end()
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test("database with owner", async () => {
|
|
156
|
-
const databaseName = "mydb"
|
|
157
|
-
const userName = "example"
|
|
158
|
-
|
|
159
|
-
// First create user
|
|
160
|
-
const create_role = createRequest("role", userName, {
|
|
161
|
-
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
162
|
-
})
|
|
163
|
-
await handler(create_role)
|
|
164
|
-
|
|
165
|
-
// Then create database with owner
|
|
166
|
-
const create_db = createRequest("database", databaseName, { Owner: userName })
|
|
167
|
-
await handler(create_db)
|
|
168
|
-
|
|
169
|
-
const connection = await newConnection()
|
|
170
|
-
try {
|
|
171
|
-
expect(await databaseExists(connection, databaseName)).toEqual(true)
|
|
172
|
-
|
|
173
|
-
// Create a table in the new database
|
|
174
|
-
const create_table = createRequest("sql", "", {
|
|
175
|
-
DatabaseName: databaseName,
|
|
176
|
-
Statement: "CREATE TABLE t(i INT)",
|
|
177
|
-
})
|
|
178
|
-
await handler(create_table)
|
|
179
|
-
|
|
180
|
-
// Verify we can login as owner and access the table
|
|
181
|
-
const userConn = await newConnection({
|
|
182
|
-
user: userName,
|
|
183
|
-
database: databaseName,
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
expect(await tableExists(userConn, databaseName, "t")).toEqual(true)
|
|
188
|
-
// Test we can insert data
|
|
189
|
-
await userConn.query("INSERT INTO t VALUES (1), (2), (3)")
|
|
190
|
-
expect(await rowCount(userConn, "t")).toEqual(3)
|
|
191
|
-
} finally {
|
|
192
|
-
await userConn.end()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Test database rename
|
|
196
|
-
const oldDatabaseName = databaseName
|
|
197
|
-
const newDatabaseName = "mydb2"
|
|
198
|
-
|
|
199
|
-
const update = updateRequest("database", oldDatabaseName, newDatabaseName, {
|
|
200
|
-
Owner: userName,
|
|
201
|
-
})
|
|
202
|
-
await expect(handler(update)).rejects.toThrow(
|
|
203
|
-
"Renaming database is not supported in MySQL."
|
|
204
|
-
)
|
|
205
|
-
} finally {
|
|
206
|
-
await connection.end()
|
|
207
|
-
}
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe("User creation", () => {
|
|
211
|
-
it("can create a user", async () => {
|
|
212
|
-
const oldRoleName = "testuser"
|
|
213
|
-
const newRoleName = "testuser2"
|
|
214
|
-
|
|
215
|
-
// Create role. We must specify a database as in mysql you cannot
|
|
216
|
-
// connect without a database.
|
|
217
|
-
const create = createRequest("role", oldRoleName, {
|
|
218
|
-
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
219
|
-
DatabaseName: DB_DEFAULT_DB,
|
|
220
|
-
})
|
|
221
|
-
await handler(create)
|
|
222
|
-
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
223
|
-
|
|
224
|
-
const connection = await newConnection()
|
|
225
|
-
try {
|
|
226
|
-
expect(await userExists(connection, oldRoleName)).toEqual(true)
|
|
227
|
-
|
|
228
|
-
// Attempt to connect as this user
|
|
229
|
-
const userConn = await newConnection({
|
|
230
|
-
user: oldRoleName,
|
|
231
|
-
})
|
|
232
|
-
await userConn.end()
|
|
233
|
-
|
|
234
|
-
// Update role - rename
|
|
235
|
-
const update = updateRequest("role", oldRoleName, newRoleName, {
|
|
236
|
-
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
237
|
-
})
|
|
238
|
-
await handler(update)
|
|
239
|
-
|
|
240
|
-
expect(await userExists(connection, oldRoleName)).toEqual(false)
|
|
241
|
-
expect(await userExists(connection, newRoleName)).toEqual(true)
|
|
242
|
-
|
|
243
|
-
// Delete role
|
|
244
|
-
const remove = deleteRequest("role", newRoleName)
|
|
245
|
-
await handler(remove)
|
|
246
|
-
expect(await userExists(connection, newRoleName)).toEqual(false)
|
|
247
|
-
} finally {
|
|
248
|
-
await connection.end()
|
|
249
|
-
}
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
it("can change a user's password", async () => {
|
|
253
|
-
const userName = "pwduser"
|
|
254
|
-
|
|
255
|
-
// Create user
|
|
256
|
-
const create = createRequest("role", userName, {
|
|
257
|
-
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
258
|
-
DatabaseName: DB_DEFAULT_DB,
|
|
259
|
-
})
|
|
260
|
-
await handler(create)
|
|
261
|
-
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
|
|
262
|
-
|
|
263
|
-
const connection = await newConnection()
|
|
264
|
-
try {
|
|
265
|
-
expect(await userExists(connection, userName)).toEqual(true)
|
|
266
|
-
|
|
267
|
-
// Test we can connect with the initial password
|
|
268
|
-
const userConn = await newConnection({
|
|
269
|
-
user: userName,
|
|
270
|
-
})
|
|
271
|
-
await userConn.end()
|
|
272
|
-
|
|
273
|
-
// Update role - change password
|
|
274
|
-
const update = updateRequest("role", userName, userName, {
|
|
275
|
-
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
|
|
276
|
-
})
|
|
277
|
-
await handler(update)
|
|
278
|
-
|
|
279
|
-
// Password changed but should still be able to connect
|
|
280
|
-
const userConn2 = await newConnection({
|
|
281
|
-
user: userName,
|
|
282
|
-
})
|
|
283
|
-
await userConn2.end()
|
|
284
|
-
} finally {
|
|
285
|
-
await connection.end()
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
test("sql execution", async () => {
|
|
291
|
-
const databaseName = "sqltest"
|
|
292
|
-
|
|
293
|
-
// Create database
|
|
294
|
-
const create_db = createRequest("database", databaseName)
|
|
295
|
-
await handler(create_db)
|
|
296
|
-
|
|
297
|
-
// Execute SQL to create table
|
|
298
|
-
const create_table = createRequest("sql", "", {
|
|
299
|
-
DatabaseName: databaseName,
|
|
300
|
-
Statement: "CREATE TABLE test_table (id INT, name VARCHAR(50))",
|
|
301
|
-
})
|
|
302
|
-
await handler(create_table)
|
|
303
|
-
|
|
304
|
-
// Execute multiple SQL statements
|
|
305
|
-
const create_multiple_tables = createRequest("sql", "", {
|
|
306
|
-
DatabaseName: databaseName,
|
|
307
|
-
Statement: "CREATE TABLE t1 (id INT); CREATE TABLE t2 (id INT);",
|
|
308
|
-
})
|
|
309
|
-
await handler(create_multiple_tables)
|
|
310
|
-
|
|
311
|
-
const connection = await newConnection({ database: databaseName })
|
|
312
|
-
try {
|
|
313
|
-
expect(await tableExists(connection, databaseName, "test_table")).toEqual(true)
|
|
314
|
-
|
|
315
|
-
// Execute SQL to insert data
|
|
316
|
-
const insert_data = createRequest("sql", "", {
|
|
317
|
-
DatabaseName: databaseName,
|
|
318
|
-
Statement: "INSERT INTO test_table VALUES (1, 'Test 1'), (2, 'Test 2')",
|
|
319
|
-
})
|
|
320
|
-
await handler(insert_data)
|
|
321
|
-
|
|
322
|
-
expect(await rowCount(connection, "test_table")).toEqual(2)
|
|
323
|
-
|
|
324
|
-
// Execute SQL with rollback for deletion test
|
|
325
|
-
const update_sql = updateRequest("sql", "dummy", "dummy", {
|
|
326
|
-
DatabaseName: databaseName,
|
|
327
|
-
Statement: "UPDATE test_table SET name = 'Updated' WHERE id = 1",
|
|
328
|
-
Rollback: "UPDATE test_table SET name = 'Test 1' WHERE id = 1",
|
|
329
|
-
})
|
|
330
|
-
await handler(update_sql)
|
|
331
|
-
|
|
332
|
-
// Verify update happened
|
|
333
|
-
const [rows] = await connection.query("SELECT name FROM test_table WHERE id = 1")
|
|
334
|
-
expect(rows[0].name).toEqual("Updated")
|
|
335
|
-
|
|
336
|
-
// Test rollback on delete
|
|
337
|
-
const delete_sql = deleteRequest("sql", "dummy", {
|
|
338
|
-
DatabaseName: databaseName,
|
|
339
|
-
Rollback: "UPDATE test_table SET name = 'Rollback test' WHERE id = 1",
|
|
340
|
-
})
|
|
341
|
-
await handler(delete_sql)
|
|
342
|
-
|
|
343
|
-
// Verify rollback executed
|
|
344
|
-
const [updated] = await connection.query("SELECT name FROM test_table WHERE id = 1")
|
|
345
|
-
expect(updated[0].name).toEqual("Rollback test")
|
|
346
|
-
} finally {
|
|
347
|
-
await connection.end()
|
|
348
|
-
}
|
|
349
|
-
})
|