cdk-rds-sql
Version:
A CDK construct that allows creating roles or users and databases an on Aurora Serverless Postgresql or Mysql/MariaDB cluster.
339 lines (304 loc) • 10.5 kB
text/typescript
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
import { Client, ClientConfig } from "pg"
import { GenericContainer, StartedTestContainer } from "testcontainers"
import { handler } from "./handler"
import {
createRequest,
updateRequest,
deleteRequest,
schemaExists,
roleExists,
databaseExists,
databaseOwnerIs,
rowCount,
roleGrantedForSchema,
} from "./util"
jest.mock("@aws-sdk/client-secrets-manager")
const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
typeof SecretsManagerClient
>
SecretsManagerClientMock.prototype.send.mockImplementation(() => {
return {
SecretString: JSON.stringify({
host: pgHost,
port: pgPort,
username: DB_MASTER_USERNAME,
password: DB_MASTER_PASSWORD,
dbname: DB_DEFAULT_DB,
engine: "postgres",
dbClusterIdentifier: "dummy",
}),
}
})
const DB_PORT = 5432
const DB_MASTER_USERNAME = "pgroot"
const DB_MASTER_PASSWORD = "masterpwd"
const DB_DEFAULT_DB = "dummy"
let pgContainer: StartedTestContainer
let pgHost: string
let pgPort: number
beforeAll(async () => {
process.env.LOGGER = "true"
process.env.SSL = "false"
process.env.CONNECTION_TIMEOUT = "5000"
pgContainer = await new GenericContainer("postgres:15")
.withExposedPorts(DB_PORT)
.withEnvironment({
POSTGRES_USER: DB_MASTER_USERNAME,
POSTGRES_PASSWORD: DB_MASTER_PASSWORD,
POSTGRES_DB: DB_DEFAULT_DB,
})
.start()
pgHost = pgContainer.getHost()
pgPort = pgContainer.getMappedPort(DB_PORT)
}, 60000)
afterAll(async () => {
if (pgContainer) {
await pgContainer.stop()
}
})
beforeEach(async () => {
jest.clearAllMocks()
// Clean up databases, schemas, and roles created by tests
const client = new Client({
host: pgHost,
port: pgPort,
database: DB_DEFAULT_DB,
user: DB_MASTER_USERNAME,
password: DB_MASTER_PASSWORD,
})
await client.connect()
try {
// Drop all databases except system ones and default
const databases = await client.query(
"SELECT datname FROM pg_database WHERE datistemplate = false AND datname != $1",
[DB_DEFAULT_DB]
)
for (const db of databases.rows) {
await client.query(`DROP DATABASE IF EXISTS "${db.datname}"`)
}
// Drop all schemas except system ones
const schemas = await client.query(
"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'public')"
)
for (const schema of schemas.rows) {
await client.query(`DROP SCHEMA IF EXISTS "${schema.schema_name}" CASCADE`)
}
// Drop all roles except system ones
const roles = await client.query(
"SELECT rolname FROM pg_roles WHERE rolname NOT LIKE 'pg_%' AND rolname != $1",
[DB_MASTER_USERNAME]
)
for (const role of roles.rows) {
await client.query(`DROP ROLE IF EXISTS "${role.rolname}"`)
}
} finally {
await client.end()
}
})
//jest.setTimeout(ms("15s"))
test("schema", async () => {
const oldSchemaName = "test"
const newSchemaName = "test2"
const create = createRequest("schema", oldSchemaName)
await handler(create)
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
const client = await newClient()
try {
expect(await schemaExists(client, oldSchemaName)).toEqual(true)
const update = updateRequest("schema", oldSchemaName, newSchemaName)
await handler(update)
expect(await schemaExists(client, oldSchemaName)).toEqual(false)
expect(await schemaExists(client, newSchemaName)).toEqual(true)
// CloudFormation will send a delete afterward, so test that too
const remove = deleteRequest("schema", newSchemaName)
await handler(remove)
expect(await schemaExists(client, newSchemaName)).toEqual(false)
// create role for testing
const roleName = "schematest"
const createRole = createRequest("role", roleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
DatabaseName: "postgres",
})
await handler(createRole)
const createWithRole = createRequest("schema", oldSchemaName, {
RoleName: roleName,
})
await handler(createWithRole)
expect(await roleGrantedForSchema(client, oldSchemaName, roleName)).toEqual(true)
const updateWithRole = updateRequest("schema", oldSchemaName, newSchemaName, {
RoleName: roleName,
})
await handler(updateWithRole)
expect(await roleGrantedForSchema(client, oldSchemaName, roleName)).toEqual(false)
expect(await roleGrantedForSchema(client, newSchemaName, roleName)).toEqual(true)
const removeWithRole = deleteRequest("schema", newSchemaName, {
RoleName: roleName,
})
await handler(removeWithRole)
expect(await roleGrantedForSchema(client, newSchemaName, roleName)).toEqual(false)
const removeRole = deleteRequest("role", roleName, {
DatabaseName: "postgres",
})
await handler(removeRole)
} finally {
await client.end()
}
})
test("role with existing database", async () => {
const oldRoleName = "example"
const newRoleName = "example2"
const create = createRequest("role", oldRoleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
DatabaseName: "postgres",
})
await handler(create)
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
const client = await newClient()
try {
expect(await roleExists(client, oldRoleName)).toEqual(true)
// Attempt to connect as this role
const client2 = await newClient({
user: oldRoleName,
})
await client2.end()
const update = updateRequest("role", oldRoleName, newRoleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
DatabaseName: "postgres",
})
await handler(update)
expect(await roleExists(client, oldRoleName)).toEqual(false)
expect(await roleExists(client, newRoleName)).toEqual(true)
// CloudFormation will send a delete afterward as we change the
// physical id, so test that too
const remove = deleteRequest("role", oldRoleName, {
DatabaseName: "postgres",
})
await handler(remove)
expect(await roleExists(client, oldRoleName)).toEqual(false)
} finally {
await client.end()
}
})
test("role without database", async () => {
const roleName = "example"
const create = createRequest("role", roleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
})
await handler(create)
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
const client = await newClient()
try {
expect(await roleExists(client, roleName)).toEqual(true)
} finally {
await client.end()
}
})
test("change role password", async () => {
const roleName = "example"
const create = createRequest("role", roleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
})
await handler(create)
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
const client = await newClient()
try {
expect(await roleExists(client, roleName)).toEqual(true)
const update = updateRequest("role", roleName, roleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
})
await handler(update)
} finally {
await client.end()
}
})
test("database", async () => {
const oldDatabaseName = "mydb"
const newDatabaseName = "mydb2"
const create = createRequest("database", oldDatabaseName)
await handler(create)
expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
const client = await newClient()
try {
expect(await databaseExists(client, oldDatabaseName)).toEqual(true)
expect(await databaseOwnerIs(client, oldDatabaseName, DB_MASTER_USERNAME)).toEqual(
true
)
const update = updateRequest("database", oldDatabaseName, newDatabaseName)
await handler(update)
expect(await databaseExists(client, oldDatabaseName)).toEqual(false)
expect(await databaseExists(client, newDatabaseName)).toEqual(true)
// CloudFormation will send a delete afterward, so test that too
const remove = deleteRequest("database", oldDatabaseName)
await handler(remove)
expect(await databaseExists(client, oldDatabaseName)).toEqual(false)
} finally {
await client.end()
}
})
test("database with owner", async () => {
const databaseName = "mydb"
const roleName = "example"
const create_role = createRequest("role", roleName, {
PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
DatabaseName: "mydb", // database does not exist yet
})
await handler(create_role)
const create_db = createRequest("database", databaseName, { Owner: "example" })
await handler(create_db)
const client = await newClient()
try {
expect(await databaseExists(client, databaseName)).toEqual(true)
expect(await databaseOwnerIs(client, databaseName, roleName)).toEqual(true)
const create_table = createRequest("sql", "", {
DatabaseName: databaseName,
Statement: "create table t(i int)",
})
await handler(create_table)
// Verify we can login as owner
{
const client2 = await newClient({
user: roleName,
database: databaseName,
})
try {
expect(await rowCount(client2, "t")).toEqual(0)
} finally {
await client2.end()
}
}
const oldDatabaseName = "mydb"
const newDatabaseName = "mydb2"
const update = updateRequest("database", oldDatabaseName, newDatabaseName, {
Owner: "example",
})
await handler(update)
expect(await databaseExists(client, oldDatabaseName)).toEqual(false)
expect(await databaseExists(client, newDatabaseName)).toEqual(true)
// Verify we can login as owner against renamed database
{
const client2 = await newClient({
user: roleName,
database: newDatabaseName,
})
try {
expect(await rowCount(client2, "t")).toEqual(0)
} finally {
await client2.end()
}
}
} finally {
await client.end()
}
})
const newClient = async (config?: ClientConfig): Promise<Client> => {
const client = new Client({
host: pgHost,
port: pgPort,
database: config && config.database ? config.database : DB_DEFAULT_DB,
user: DB_MASTER_USERNAME,
password: DB_MASTER_PASSWORD,
})
await client.connect()
return client
}