iam finally works again, still a bunch of cleanup to do

This commit is contained in:
Day Matchullis
2026-01-06 22:30:54 -07:00
parent d55d5651a3
commit 477d790de4
12 changed files with 142 additions and 140 deletions

View File

@@ -102,7 +102,7 @@
<div class="form-group">
<label for="AWS Region">AWS Region</label>
<masked-input :value="config.iamAuthOptions.awsRegion" :privacy-mode="privacyMode" @input="val => config.iamAuthOptions.awsRegion = val" :type="'password'" />
<masked-input :value="config.iamAuthOptions.awsRegion" :privacy-mode="privacyMode" @input="val => config.iamAuthOptions.awsRegion = val" />
</div>
<div v-show="isRedshift">

View File

@@ -63,10 +63,13 @@ export default {
this.$root.$emit(AppEvent.upgradeModal, "Upgrade required to use this authentication type");
this.authType = 'default'
} else {
this.config.iamAuthOptions.authType = this.authType
this.config.azureAuthOptions.azureAuthType = this.authType
this.azureAuthEnabled = this.authType === AzureAuthType.CLI
this.iamAuthenticationEnabled = typeof this.authType === 'string' && this.authType.includes('iam')
if (typeof this.authType === 'string' && this.authType.includes('iam')) {
this.iamAuthenticationEnabled = true;
this.config.iamAuthOptions.authType = this.authType;
} else if (this.authType === AzureAuthType.CLI) {
this.azureAuthEnabled = true;
this.config.azureAuthOptions.azureAuthType = this.authType;
}
}
}

View File

@@ -71,10 +71,13 @@ export default {
this.$root.$emit(AppEvent.upgradeModal, "Upgrade required to use this authentication type");
this.authType = 'default'
} else {
this.config.iamAuthOptions.authType = this.authType
this.iamAuthenticationEnabled = typeof this.authType === 'string' && this.authType.includes('iam')
this.config.azureAuthOptions.azureAuthType = this.authType
this.azureAuthEnabled = this.authType === AzureAuthType.CLI
if (typeof this.authType === 'string' && this.authType.includes('iam')) {
this.iamAuthenticationEnabled = true;
this.config.iamAuthOptions.authType = this.authType;
} else if (this.authType === AzureAuthType.CLI) {
this.azureAuthEnabled = true;
this.config.azureAuthOptions.azureAuthType = this.authType;
}
}
}

View File

@@ -27,18 +27,20 @@ export default {
components: {CommonIam, CommonServerInputs, CommonAdvanced },
data() {
return {
iamAuthenticationEnabled: this.config.redshiftOptions?.authType?.includes?.('iam'),
authType: this.config.redshiftOptions?.authType || 'default',
iamAuthenticationEnabled: this.config.iamAuthOptions?.authType?.includes?.('iam'),
authType: this.config.iamAuthOptions?.authType || 'default',
authTypes: [{ name: 'Username / Password', value: 'default' }, ...IamAuthTypes]
}
},
watch: {
async authType() {
this.iamAuthenticationEnabled = this.authType.includes('iam');
this.config.redshiftOptions.authType = this.authType;
if (this.authType.includes('iam')) {
this.iamAuthenticationEnabled = true;
this.config.iamAuthOptions.authType = this.authType
}
},
iamAuthenticationEnabled() {
this.config.redshiftOptions.iamAuthenticationEnabled = this.iamAuthenticationEnabled
this.config.iamAuthOptions.iamAuthenticationEnabled = this.iamAuthenticationEnabled
}
},
props: ['config'],

View File

@@ -1,6 +1,5 @@
import { GetClusterCredentialsCommand, RedshiftClient } from '@aws-sdk/client-redshift';
import { RedshiftServerlessClient, GetCredentialsCommand } from "@aws-sdk/client-redshift-serverless";
import {spawn} from "child_process";
import rawLog from '@bksLogger';
import { IamAuthOptions, IDbConnectionServerConfig } from '../types';
@@ -31,62 +30,6 @@ export interface TemporaryClusterCredentials {
dbPassword: string;
expiration: Date;
}
export async function getAWSCLIToken(server: IDbConnectionServerConfig, options: IamAuthOptions): Promise<string> {
if (!options?.cliPath) {
throw new Error('AZ command not specified');
}
const extraArgs = []
if(options.awsProfile){
extraArgs.push('--profile', options.awsProfile)
}
return new Promise<string>((resolve, reject) => {
const proc = spawn(options.cliPath, [
'rds',
'generate-db-auth-token',
'--hostname',
server.host,
'--port',
server.port.toString(),
'--region',
options.awsRegion,
'--username',
server.user,
...extraArgs
]);
let stdout = '';
let stderr = '';
proc.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
proc.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
proc.on('error', (err) => {
reject(err);
});
proc.on('close', (code) => {
if (code === 0) {
try {
resolve(stdout.trim());
} catch (err) {
reject(`Failed to parse token JSON: ${err}\nRaw output: ${stdout}`);
}
} else {
reject(`Process exited with code ${code}\nSTDERR: ${stderr}\nSTDOUT: ${stdout}`);
}
});
});
}
/**
* RedshiftCredentialResolver provides the ability to use temporary cluster credentials to access
* an Amazon Redshift cluster.

View File

@@ -67,7 +67,6 @@ import { GenericBinaryTranscoder } from "../serialization/transcoders";
import { Version, isVersionLessThanOrEqual, parseVersion } from "@/common/version";
import globals from '../../../common/globals';
import {AzureAuthService} from "@/lib/db/authentication/azure";
import { getAWSCLIToken } from "../authentication/amazon-redshift";
type ResultType = {
tableName?: string
@@ -136,13 +135,9 @@ async function configDatabase(
database: IDbConnectionDatabase
): Promise<mysql.PoolOptions> {
let awsCLIToken = undefined;
if( server.config.iamAuthOptions?.authType === 'iam_cli') {
awsCLIToken = await getAWSCLIToken(server.config, server.config.iamAuthOptions);
}
let iamToken = undefined;
if(server.config.iamAuthOptions?.iamAuthenticationEnabled){
awsCLIToken = await refreshTokenIfNeeded(server.config?.iamAuthOptions, server, server.config.port || 5432)
iamToken = await refreshTokenIfNeeded(server.config?.iamAuthOptions, server, server.config.port || 5432)
}
const config: mysql.PoolOptions = {
@@ -152,7 +147,7 @@ async function configDatabase(
host: server.config.host,
port: server.config.port,
user: server.config.user,
password: awsCLIToken || server.config.password || undefined,
password: iamToken || server.config.password || undefined,
database: database.database,
multipleStatements: true,
dateStrings: true,

View File

@@ -27,7 +27,6 @@ import BksConfig from '@/common/bksConfig';
import { IDbConnectionServer } from '../backendTypes';
import { GenericBinaryTranscoder } from "../serialization/transcoders";
import {AzureAuthService} from "@/lib/db/authentication/azure";
import { getAWSCLIToken } from '../authentication/amazon-redshift';
const PD = PostgresData
@@ -138,6 +137,8 @@ export class PostgresClient extends BasicDatabaseClient<QueryResult, PoolClient>
const dbConfig = await this.configDatabase(this.server, this.database);
log.info("CONFIG: ", dbConfig)
this.conn = {
pool: new pg.Pool(dbConfig)
};
@@ -1494,19 +1495,15 @@ export class PostgresClient extends BasicDatabaseClient<QueryResult, PoolClient>
protected async configDatabase(server: IDbConnectionServer, database: { database: string}) {
let awsCLIToken = undefined;
if( server.config.iamAuthOptions?.authType === 'iam_cli') {
awsCLIToken = await getAWSCLIToken(server.config, server.config.iamAuthOptions);
}
let iamToken = undefined;
if(server.config.iamAuthOptions?.iamAuthenticationEnabled){
awsCLIToken = await refreshTokenIfNeeded(server.config?.iamAuthOptions, server, server.config.port || 5432)
iamToken = await refreshTokenIfNeeded(server.config?.iamAuthOptions, server, server.config.port || 5432)
}
const config: PoolConfig = {
host: server.config.host,
port: server.config.port || undefined,
password: awsCLIToken || server.config.password || undefined,
password: iamToken || server.config.password || undefined,
database: database.database,
max: BksConfig.db.postgres.maxConnections, // max idle connections per time (30 secs)
connectionTimeoutMillis: BksConfig.db.postgres.connectionTimeout,

View File

@@ -4,15 +4,17 @@ import logRaw from '@bksLogger'
import { TableChanges, TableDelete, TableFilter, TableInsert, TableUpdate, BuildInsertOptions } from '../models'
import { joinFilters } from '@/common/utils'
import { IdentifyResult } from 'sql-query-identifier/lib/defines'
import {fromIni} from "@aws-sdk/credential-providers";
import {Signer} from "@aws-sdk/rds-signer";
import { fromIni } from "@aws-sdk/credential-providers";
import { Signer } from "@aws-sdk/rds-signer";
import globals from "@/common/globals";
import {
AWSCredentials
} from "@/lib/db/authentication/amazon-redshift";
import {RedshiftOptions} from "@/lib/db/types";
import {AuthOptions} from "@/lib/db/authentication/azure";
import { IamAuthOptions, IamAuthType, IDbConnectionServerConfig } from "@/lib/db/types";
import { AuthOptions } from "@/lib/db/authentication/azure";
import { spawn } from "child_process";
import { loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
import { AwsCredentialIdentity, RuntimeConfigAwsCredentialIdentityProvider } from '@aws-sdk/types'
const log = logRaw.scope('db/util')
@@ -354,31 +356,41 @@ export const errorMessages = {
maxReservedConnections: 'You have reserved the max connections available for manual transactions. Stop one of your active transactions to start a new one.'
}
export async function resolveAWSCredentials(redshiftOptions: RedshiftOptions): Promise<AWSCredentials> {
if (redshiftOptions.accessKeyId && redshiftOptions.secretAccessKey) {
export async function resolveAWSCredentials(iamOptions: IamAuthOptions): Promise<AWSCredentials> {
if (iamOptions.accessKeyId && iamOptions.secretAccessKey) {
return {
accessKeyId: redshiftOptions.accessKeyId,
secretAccessKey: redshiftOptions.secretAccessKey,
accessKeyId: iamOptions.accessKeyId,
secretAccessKey: iamOptions.secretAccessKey,
};
}
// Fallback to AWS profile-based credentials
const provider = fromIni({
profile: redshiftOptions.awsProfile || "default",
profile: iamOptions.awsProfile || "default",
});
return provider();
}
export async function getIAMPassword(redshiftOptions: RedshiftOptions, hostname: string, port: number, username: string): Promise<string> {
const {awsProfile, accessKeyId, secretAccessKey} = redshiftOptions
let {awsRegion: region} = redshiftOptions
export async function getIAMPassword(iamOptions: IamAuthOptions, hostname: string, port: number, username: string): Promise<string> {
const {
awsProfile,
accessKeyId,
secretAccessKey,
authType
} = iamOptions;
let credentials: {
profile?: string,
accessKeyId?: string,
secretAccessKey?: string,
} = {
profile: awsProfile || "default"
let { awsRegion: region } = iamOptions;
let credentials: AwsCredentialIdentity | RuntimeConfigAwsCredentialIdentityProvider;
if (authType === IamAuthType.Key) {
credentials = {
accessKeyId,
secretAccessKey,
}
} else {
const profileCreds = { profile: awsProfile || "default" };
credentials = fromIni(profileCreds);
}
if (!region) {
@@ -389,48 +401,94 @@ export async function getIAMPassword(redshiftOptions: RedshiftOptions, hostname:
}
}
if(accessKeyId && secretAccessKey) {
credentials = {
profile: awsProfile || "default",
accessKeyId,
secretAccessKey
}
}
const nodeProviderChainCredentials = fromIni(credentials);
const signer = new Signer({
credentials: nodeProviderChainCredentials,
credentials,
hostname,
region,
port,
username,
});
return await signer.getAuthToken();
}
let resolvedPw: string | undefined;
let tokenExpiryTime: number | null = null;
let redshiftOptionsCheck: RedshiftOptions | null = null
export async function refreshTokenIfNeeded(redshiftOptions: RedshiftOptions, server: any, port: number): Promise<string> {
if(!redshiftOptions?.iamAuthenticationEnabled){
export async function refreshTokenIfNeeded(iamOptions: IamAuthOptions, server: any, port: number): Promise<string> {
if(!iamOptions?.iamAuthenticationEnabled){
return null
}
const now = Date.now();
let resolvedPw: string = null;
if (redshiftOptionsCheck != redshiftOptions || (!resolvedPw || !tokenExpiryTime || now >= tokenExpiryTime - globals.iamRefreshBeforeTime)) { // Refresh 2 minutes before expiry
redshiftOptionsCheck = redshiftOptions
log.info("Refreshing IAM token...");
if (iamOptions?.authType === IamAuthType.CLI) {
resolvedPw = await getAWSCLIToken(
server.config,
iamOptions
);
} else {
// TODO (@day): why are we passing in the port like this?!?
resolvedPw = await getIAMPassword(
redshiftOptions,
iamOptions,
server.config.host,
server.config.port || port,
server.config.user
);
tokenExpiryTime = now + globals.iamExpiryTime; // Tokens last 15 minutes
}
return resolvedPw;
}
export async function getAWSCLIToken(server: IDbConnectionServerConfig, options: IamAuthOptions): Promise<string> {
if (!options?.cliPath) {
throw new Error('AZ command not specified');
}
const extraArgs = []
if(options.awsProfile){
extraArgs.push('--profile', options.awsProfile)
}
return new Promise<string>((resolve, reject) => {
const proc = spawn(options.cliPath, [
'rds',
'generate-db-auth-token',
'--hostname',
server.host,
'--port',
server.port.toString(),
'--region',
options.awsRegion,
'--username',
server.user,
...extraArgs
]);
let stdout = '';
let stderr = '';
proc.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
proc.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
proc.on('error', (err) => {
reject(err);
});
proc.on('close', (code) => {
if (code === 0) {
try {
resolve(stdout.trim());
} catch (err) {
reject(`Failed to parse token JSON: ${err}\nRaw output: ${stdout}`);
}
} else {
reject(`Process exited with code ${code}\nSTDERR: ${stderr}\nSTDOUT: ${stdout}`);
}
});
});
}

View File

@@ -59,10 +59,16 @@ export enum AzureAuthType {
CLI
}
export enum IamAuthType {
Key = 'iam_key',
File = 'iam_file',
CLI = 'iam_cli'
}
export const IamAuthTypes = [
{ name: 'IAM Authentication Using Access Key and Secret Key', value: 'iam_key' },
{ name: 'IAM Authentication Using Credentials File', value: 'iam_file' },
{ name: 'AWS CLI Authentication', value: 'iam_cli' }
{ name: 'IAM Authentication Using Access Key and Secret Key', value: IamAuthType.Key },
{ name: 'IAM Authentication Using Credentials File', value: IamAuthType.File },
{ name: 'AWS CLI Authentication', value: IamAuthType.CLI }
]
// supported auth types that actually work :roll_eyes: default i'm looking at you
@@ -90,7 +96,7 @@ export interface IamAuthOptions {
secretAccessKey?: string;
awsRegion?: string;
isServerless?: boolean;
authType?: string;
authType?: IamAuthType;
cliPath?: string;
}

View File

@@ -143,7 +143,6 @@ services:
mariadb:
image: mariadb
platform: linux/amd64
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: test
@@ -156,7 +155,6 @@ services:
image: mysql:8.0.21
platform: linux/amd64
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: test
@@ -169,7 +167,6 @@ services:
image: mysql:5.7.22
platform: linux/amd64
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: test
@@ -181,7 +178,6 @@ services:
mysql4.1:
image: vettadock/mysql-old:4.1
platform: linux/amd64
restart: unless-stopped
ports:
- 3309:3306
sqlserver:
@@ -190,7 +186,6 @@ services:
volumes:
- mssql:/var/opt/mssql/data
- ./dev/docker_sqlserver:/docker_init
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
MSSSQL_PID: "Express"

View File

@@ -13,7 +13,7 @@ You will then need to create an IAM user and attach the `AmazonRDSFullAccess` po
You can also use a similar policy to the below:
``
```json
{
"Version": "2012-10-17",
"Statement": [
@@ -28,7 +28,7 @@ You can also use a similar policy to the below:
}
]
}
``
```
## Connecting to Amazon RDS