diff --git a/apps/studio/src/components/NotificationManager.vue b/apps/studio/src/components/NotificationManager.vue index 41aa6f417..3d7315d34 100644 --- a/apps/studio/src/components/NotificationManager.vue +++ b/apps/studio/src/components/NotificationManager.vue @@ -46,7 +46,7 @@ export default Vue.extend({ clearTimeout(this.timeoutID) this.timeoutID = null } - if (!this.isCommunity) { + if (!this.isCommunity) { return } diff --git a/apps/studio/src/components/connection/CommonIam.vue b/apps/studio/src/components/connection/CommonIam.vue index 9f927c029..7b2fbca0c 100644 --- a/apps/studio/src/components/connection/CommonIam.vue +++ b/apps/studio/src/components/connection/CommonIam.vue @@ -102,7 +102,7 @@
- +
diff --git a/apps/studio/src/components/connection/MysqlForm.vue b/apps/studio/src/components/connection/MysqlForm.vue index 45e3b11f2..7f5902e25 100644 --- a/apps/studio/src/components/connection/MysqlForm.vue +++ b/apps/studio/src/components/connection/MysqlForm.vue @@ -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; + } } } diff --git a/apps/studio/src/components/connection/PostgresForm.vue b/apps/studio/src/components/connection/PostgresForm.vue index 700e493f8..5e4e52a64 100644 --- a/apps/studio/src/components/connection/PostgresForm.vue +++ b/apps/studio/src/components/connection/PostgresForm.vue @@ -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; + } } } diff --git a/apps/studio/src/components/connection/RedshiftForm.vue b/apps/studio/src/components/connection/RedshiftForm.vue index 48a0a4149..0c0b6bb51 100644 --- a/apps/studio/src/components/connection/RedshiftForm.vue +++ b/apps/studio/src/components/connection/RedshiftForm.vue @@ -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'], diff --git a/apps/studio/src/lib/db/authentication/amazon-redshift.ts b/apps/studio/src/lib/db/authentication/amazon-redshift.ts index 0f4c29a2c..fd0d1dfbe 100644 --- a/apps/studio/src/lib/db/authentication/amazon-redshift.ts +++ b/apps/studio/src/lib/db/authentication/amazon-redshift.ts @@ -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 { - if (!options?.cliPath) { - throw new Error('AZ command not specified'); - } - - const extraArgs = [] - - if(options.awsProfile){ - extraArgs.push('--profile', options.awsProfile) - } - - return new Promise((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. diff --git a/apps/studio/src/lib/db/clients/mysql.ts b/apps/studio/src/lib/db/clients/mysql.ts index 55c3373a9..dd75e145a 100644 --- a/apps/studio/src/lib/db/clients/mysql.ts +++ b/apps/studio/src/lib/db/clients/mysql.ts @@ -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 { - 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, diff --git a/apps/studio/src/lib/db/clients/postgresql.ts b/apps/studio/src/lib/db/clients/postgresql.ts index dc18680da..8eeb9b9e2 100644 --- a/apps/studio/src/lib/db/clients/postgresql.ts +++ b/apps/studio/src/lib/db/clients/postgresql.ts @@ -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 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 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, diff --git a/apps/studio/src/lib/db/clients/utils.ts b/apps/studio/src/lib/db/clients/utils.ts index 0ad9f3f79..6a141415a 100644 --- a/apps/studio/src/lib/db/clients/utils.ts +++ b/apps/studio/src/lib/db/clients/utils.ts @@ -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 { - if (redshiftOptions.accessKeyId && redshiftOptions.secretAccessKey) { +export async function resolveAWSCredentials(iamOptions: IamAuthOptions): Promise { + 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 { - const {awsProfile, accessKeyId, secretAccessKey} = redshiftOptions - let {awsRegion: region} = redshiftOptions +export async function getIAMPassword(iamOptions: IamAuthOptions, hostname: string, port: number, username: string): Promise { + 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 { - if(!redshiftOptions?.iamAuthenticationEnabled){ +export async function refreshTokenIfNeeded(iamOptions: IamAuthOptions, server: any, port: number): Promise { + 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 { + if (!options?.cliPath) { + throw new Error('AZ command not specified'); + } + + const extraArgs = [] + + if(options.awsProfile){ + extraArgs.push('--profile', options.awsProfile) + } + + return new Promise((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}`); + } + }); + }); +} + diff --git a/apps/studio/src/lib/db/types.ts b/apps/studio/src/lib/db/types.ts index df4ecf058..57ca805bd 100644 --- a/apps/studio/src/lib/db/types.ts +++ b/apps/studio/src/lib/db/types.ts @@ -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; } diff --git a/docker-compose.yml b/docker-compose.yml index 6f5094deb..359feb00e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/docs/user_guide/connecting/amazon-rds.md b/docs/user_guide/connecting/amazon-rds.md index f8b5ddc59..95eaf9831 100644 --- a/docs/user_guide/connecting/amazon-rds.md +++ b/docs/user_guide/connecting/amazon-rds.md @@ -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