mirror of
https://github.com/beekeeper-studio/beekeeper-studio.git
synced 2026-03-13 10:12:54 +08:00
iam finally works again, still a bunch of cleanup to do
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user