UNPKG

cdk-rds-sql

Version:

A CDK construct that allows creating roles or users and databases an on Aurora Serverless Postgresql or Mysql/MariaDB cluster.

233 lines (198 loc) 7.4 kB
import * as fs from "fs" import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract" import { EngineDatabaseProperties, EngineRoleProperties, EngineSchemaProperties, EngineSqlProperties, } from "./types" export class MysqlEngine extends AbstractEngine { createDatabase(resourceId: string, props: EngineDatabaseProperties): string[] { const sql = [`CREATE DATABASE IF NOT EXISTS \`${resourceId}\``] if (props.Owner) { sql.push(`GRANT ALL PRIVILEGES ON \`${resourceId}\`.* TO '${props.Owner}'@'%'`) sql.push("FLUSH PRIVILEGES") } return sql } updateDatabase(): string[] { throw new Error("Renaming database is not supported in MySQL.") } deleteDatabase(resourceId: string, _masterUser: string): string[] { return [`DROP DATABASE IF EXISTS \`${resourceId}\``] } async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> { const sql: string[] = [] if (props.EnableIamAuth) { // Create user for IAM authentication sql.push( `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'` ) } else { // Create user with password authentication if (!props.PasswordArn) throw new Error("No PasswordArn provided") const password = await this.getPassword(props.PasswordArn) if (!password) throw `Cannot parse password from ${props.PasswordArn}` sql.push( `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'` ) } if (props.DatabaseName) { sql.push( `GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'` ) } sql.push("FLUSH PRIVILEGES") return sql } async updateRole( resourceId: string, oldResourceId: string, props: EngineRoleProperties, oldProps: EngineRoleProperties ): Promise<string[]> { const sql: string[] = [] if (oldResourceId !== resourceId) { // MySQL doesn't allow renaming users directly, we need to create a new one and drop the old one if (props.EnableIamAuth) { // Create new user with IAM auth sql.push( `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'` ) } else if (props?.PasswordArn) { // Create new user with password auth const password = await this.getPassword(props.PasswordArn) if (!password) throw `Cannot parse password from ${props.PasswordArn}` sql.push( `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'` ) } else { // If no password is provided, create user with a random password then expire it sql.push(`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY UUID()`) sql.push(`ALTER USER '${resourceId}'@'%' PASSWORD EXPIRE`) } // Drop the old user sql.push(`DROP USER IF EXISTS '${oldResourceId}'@'%'`) } else { // Handle authentication method changes for existing user if (props?.EnableIamAuth && !oldProps?.EnableIamAuth) { // Switching from password to IAM auth - need to recreate user sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`) sql.push( `CREATE USER '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'` ) } else if (!props?.EnableIamAuth && oldProps?.EnableIamAuth) { // Switching from IAM to password auth - need to recreate user sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`) if (props?.PasswordArn) { const password = await this.getPassword(props.PasswordArn) if (!password) throw `Cannot parse password from ${props.PasswordArn}` sql.push(`CREATE USER '${resourceId}'@'%' IDENTIFIED BY '${password}'`) } } else if (!props?.EnableIamAuth && props?.PasswordArn) { // Just update the password for password auth const password = await this.getPassword(props.PasswordArn) if (!password) throw `Cannot parse password from ${props.PasswordArn}` sql.push(`ALTER USER '${resourceId}'@'%' IDENTIFIED BY '${password}'`) } } // Check if database name has changed if ( oldProps?.DatabaseName && props?.DatabaseName && oldProps.DatabaseName !== props.DatabaseName ) { // Revoke from old database sql.push( `REVOKE ALL PRIVILEGES ON \`${oldProps.DatabaseName}\`.* FROM '${resourceId}'@'%'` ) } if (props?.DatabaseName) { sql.push( `GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'` ) } if (sql.length > 0) { sql.push("FLUSH PRIVILEGES") } return sql } deleteRole(resourceId: string, props: EngineRoleProperties): string[] { const sql: string[] = [] if (props?.DatabaseName) { sql.push( `REVOKE ALL PRIVILEGES ON \`${props.DatabaseName}\`.* FROM '${resourceId}'@'%'` ) } sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`) sql.push("FLUSH PRIVILEGES") return sql } createSchema(_resourceId: string, _props: EngineSchemaProperties): string[] { throw new Error("Schemas are not supported in MySQL/MariaDB") } updateSchema( _resourceId: string, _oldResourceId: string, _props: EngineSchemaProperties ): string[] { throw new Error("Schemas are not supported in MySQL/MariaDB") } deleteSchema(_resourceId: string, _props: EngineSchemaProperties): string[] { throw new Error("Schemas are not supported in MySQL/MariaDB") } createSql(_resourceId: string, props: EngineSqlProperties): string { return props?.Statement || "" } updateSql( _resourceId: string, _oldResourceId: string, props: EngineSqlProperties ): string { return props?.Statement || "" } deleteSql(_resourceId: string, props: EngineSqlProperties): string { return props?.Rollback || "" } async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> { // Dynamic import to avoid bundling issues const { createConnection } = await import("mysql2/promise") const isSslEnabled = process.env.SSL ? JSON.parse(process.env.SSL) : true const sslOptions = isSslEnabled ? { ssl: { ca: fs.readFileSync(`${process.env.LAMBDA_TASK_ROOT}/global-bundle.pem`), rejectUnauthorized: true, }, } : {} const connectionConfig = { host: config.host, port: config.port, user: config.user, password: config.password, database: config.database, connectTimeout: 30000, multipleStatements: true, ...sslOptions, } this.log( `Connecting to MySQL/MariaDB host ${connectionConfig.host}:${ connectionConfig.port }${isSslEnabled ? " using a secure connection" : ""}, database ${ connectionConfig.database } as ${connectionConfig.user}` ) this.log("Executing SQL", sql) const connection = await createConnection(connectionConfig) try { if (typeof sql === "string") { return await connection.query(sql) } else if (sql) { return await Promise.all(sql.map((statement) => connection.query(statement))) } } finally { await connection.end() } } }