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