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