feat(backend-defaults): improve AWS RDS IAM auth error handling and token expiry
Signed-off-by: Roland Fuszenecker <roland.fuszenecker@seon.io>
This commit is contained in:
+2
-2
@@ -644,9 +644,9 @@ export interface Config {
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* The port number the database is listening on. Defaults to 5432.
|
||||
* The port number the database is listening on.
|
||||
*/
|
||||
port?: number;
|
||||
port: number;
|
||||
/**
|
||||
* The database user to authenticate as. This user must have the `rds_iam` role granted.
|
||||
*/
|
||||
|
||||
@@ -649,6 +649,7 @@ describe('postgres', () => {
|
||||
connection: {
|
||||
type: 'rds',
|
||||
host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
@@ -662,28 +663,47 @@ describe('postgres', () => {
|
||||
expect(conn2.password).toBe('token-2');
|
||||
});
|
||||
|
||||
it('defaults port to 5432 for rds connections', async () => {
|
||||
it('returns an expirationChecker that reflects the token TTL', async () => {
|
||||
const { Signer } = jest.requireMock('@aws-sdk/rds-signer') as jest.Mocked<
|
||||
typeof import('@aws-sdk/rds-signer')
|
||||
>;
|
||||
|
||||
Signer.prototype.getAuthToken.mockResolvedValue('mock-iam-token');
|
||||
|
||||
await buildPgDatabaseConfig(
|
||||
const configResult = await buildPgDatabaseConfig(
|
||||
new ConfigReader({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
type: 'rds',
|
||||
host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(Signer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ port: 5432 }),
|
||||
);
|
||||
const conn = await (configResult.connection as () => Promise<any>)();
|
||||
|
||||
expect(conn.expirationChecker).toBeInstanceOf(Function);
|
||||
// Token was just issued, so it should not yet be considered expired.
|
||||
expect(conn.expirationChecker()).toBe(false);
|
||||
});
|
||||
|
||||
it('throws when port is missing for rds connection', async () => {
|
||||
await expect(
|
||||
buildPgDatabaseConfig(
|
||||
new ConfigReader({
|
||||
client: 'pg',
|
||||
connection: {
|
||||
type: 'rds',
|
||||
host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
|
||||
user: 'postgres',
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow(/connection\.port/);
|
||||
});
|
||||
|
||||
it('falls back to AWS_REGION env var when region is not set in config', async () => {
|
||||
@@ -703,6 +723,7 @@ describe('postgres', () => {
|
||||
connection: {
|
||||
type: 'rds',
|
||||
host: 'mydb.cluster.us-east-1.rds.amazonaws.com',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
},
|
||||
}),
|
||||
@@ -727,14 +748,13 @@ describe('postgres', () => {
|
||||
client: 'pg',
|
||||
connection: {
|
||||
type: 'rds',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow(
|
||||
/Missing host in connection config for AWS RDS IAM auth/,
|
||||
);
|
||||
).rejects.toThrow(/connection\.host/);
|
||||
});
|
||||
|
||||
it('throws when user is missing for rds connection', async () => {
|
||||
@@ -745,13 +765,12 @@ describe('postgres', () => {
|
||||
connection: {
|
||||
type: 'rds',
|
||||
host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
|
||||
port: 5432,
|
||||
region: 'eu-west-1',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow(
|
||||
/Missing user in connection config for AWS RDS IAM auth/,
|
||||
);
|
||||
).rejects.toThrow(/connection\.user/);
|
||||
});
|
||||
|
||||
it('throws when region is missing and no env var is set for rds connection', async () => {
|
||||
@@ -768,6 +787,7 @@ describe('postgres', () => {
|
||||
connection: {
|
||||
type: 'rds',
|
||||
host: 'mydb.cluster.eu-west-1.rds.amazonaws.com',
|
||||
port: 5432,
|
||||
user: 'postgres',
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -277,24 +277,21 @@ export async function buildRdsPgConfig(config: Config): Promise<Knex.Config> {
|
||||
const { Signer } =
|
||||
require('@aws-sdk/rds-signer') as typeof import('@aws-sdk/rds-signer');
|
||||
|
||||
const rawConfig = config.get() as Record<string, unknown>;
|
||||
const normalized = normalizeConnection(rawConfig.connection as any);
|
||||
const sanitizedConnection = omit(normalized, [
|
||||
'type',
|
||||
'region',
|
||||
]) as Partial<Knex.StaticConnectionConfig>;
|
||||
|
||||
const hostname = (normalized as any).host as string;
|
||||
if (!hostname) {
|
||||
throw new Error('Missing host in connection config for AWS RDS IAM auth');
|
||||
}
|
||||
const port = ((normalized as any).port as number | undefined) ?? 5432;
|
||||
const username = (normalized as any).user as string;
|
||||
if (!username) {
|
||||
throw new Error('Missing user in connection config for AWS RDS IAM auth');
|
||||
let hostname: string;
|
||||
let port: number;
|
||||
let username: string;
|
||||
try {
|
||||
hostname = config.getString('connection.host');
|
||||
port = config.getNumber('connection.port');
|
||||
username = config.getString('connection.user');
|
||||
} catch (err) {
|
||||
throw new ForwardedError(
|
||||
'AWS RDS IAM auth: missing required database connection config — make sure connection.host, connection.port, and connection.user are set and any environment variables they reference are set',
|
||||
err,
|
||||
);
|
||||
}
|
||||
const region =
|
||||
((normalized as any).region as string | undefined) ??
|
||||
config.getOptionalString('connection.region') ??
|
||||
process.env.AWS_REGION ??
|
||||
process.env.AWS_DEFAULT_REGION;
|
||||
if (!region) {
|
||||
@@ -303,17 +300,33 @@ export async function buildRdsPgConfig(config: Config): Promise<Knex.Config> {
|
||||
);
|
||||
}
|
||||
|
||||
const rawConfig = config.get() as Record<string, unknown>;
|
||||
const sanitizedConnection = omit(
|
||||
config.get('connection') as Record<string, unknown>,
|
||||
['type', 'region'],
|
||||
) as Partial<Knex.StaticConnectionConfig>;
|
||||
|
||||
const signer = new Signer({ hostname, port, username, region });
|
||||
|
||||
// RDS IAM auth tokens are valid for 15 minutes. Renew 1 minute early so
|
||||
// that pooled connections are refreshed before the token actually expires.
|
||||
const tokenTtlMs = 15 * 60 * 1000;
|
||||
const renewalOffsetMs = 60 * 1000;
|
||||
|
||||
async function getConnectionConfig() {
|
||||
try {
|
||||
const password = await signer.getAuthToken();
|
||||
return { ...sanitizedConnection, password };
|
||||
const tokenExpiration = Date.now() + tokenTtlMs - renewalOffsetMs;
|
||||
return {
|
||||
...sanitizedConnection,
|
||||
password,
|
||||
expirationChecker: () => tokenExpiration <= Date.now(),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`AWS RDS IAM auth token acquisition failed for ${username}@${hostname}:${port}: ${err}`,
|
||||
throw new ForwardedError(
|
||||
`AWS RDS IAM auth token acquisition failed for ${username}@${hostname}:${port}`,
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user