From 99b7c87c3464b0528c62fa29cba9e026c9c10da3 Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 13 Dec 2022 15:03:16 +0200 Subject: [PATCH 1/3] add service accounts support --- README.md | 57 +++++++++++++---- src/auth/index.ts | 78 +++++++++++++++++++++--- src/common/api.ts | 1 + src/common/errors.ts | 17 ++++++ src/core/index.ts | 11 ++-- src/service/index.ts | 4 +- src/types.ts | 26 ++++++-- test/integration/engine.test.ts | 2 +- test/integration/serviceAccounts.test.ts | 39 ++++++++++++ 9 files changed, 200 insertions(+), 35 deletions(-) create mode 100644 test/integration/serviceAccounts.test.ts diff --git a/README.md b/README.md index e2a3f357..92d59b81 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,10 @@ import { Firebolt } from 'firebolt-sdk' const firebolt = Firebolt(); const connection = await firebolt.connect({ - username: process.env.FIREBOLT_USERNAME, - password: process.env.FIREBOLT_PASSWORD, + auth: { + username: process.env.FIREBOLT_USERNAME, + password: process.env.FIREBOLT_PASSWORD, + }, database: process.env.FIREBOLT_DATABASE, engineName: process.env.FIREBOLT_ENGINE_NAME }); @@ -62,6 +64,7 @@ console.log(rows) * Create connection * ConnectionOptions * AccessToken + * Service account * engineName * Test connection * Engine URL @@ -112,10 +115,22 @@ const connection = await firebolt.connect(connectionOptions); #### ConnectionOptions ```typescript +type UsernamePasswordAuth = { + username: string; + password: string; +}; + +type AccessTokenAuth = { + accessToken: string; +}; + +type ServiceAccountAuth = { + client_id: string; + client_secret: string; +}; + type ConnectionOptions = { - username?: string; - password?: string; - accessToken?: string; + auth: UsernamePasswordAuth | AccessTokenAuth | ServiceAccountAuth; database: string; engineName?: string; engineEndpoint?: string; @@ -137,13 +152,30 @@ and pass accessToken when creating the connection ```typescript const connection = await firebolt.connect({ - accessToken: "access_token", + auth: { + accessToken: "access_token", + }, engineName: 'engine_name', account: 'account_name', database: 'database', }); ``` + +#### Service account +Instead of passing username/password, you can also use service account + +```typescript +const connection = await firebolt.connect({ + auth: { + client_id: 'b1c4918c-e07e-4ab2-868b-9ae84f208d26'; + client_secret: 'secret'; + }, + engineName: 'engine_name', + account: 'account_name', + database: 'database', +}); +``` ### Test connection TODO: write motivation @@ -430,14 +462,15 @@ providing only auth credentials ```typescript import { FireboltResourceManager } from 'firebolt-sdk' -const authOptions = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, +const resourceManager = FireboltResourceManager(); +await resourceManager.authenticate({ + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string, + }, account: process.env.ACCOUNT_NAME as string -}; +}); -const resourceManager = FireboltResourceManager(); -await resourceManager.authenticate(authOptions); const engine = await resourceManager.engine.getByName( process.env.FIREBOLT_ENGINE_NAME as string ); diff --git a/src/auth/index.ts b/src/auth/index.ts index 1f1e4fba..7c67418d 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,5 +1,11 @@ -import { LOGIN, REFRESH } from "../common/api"; -import { Context, AuthOptions } from "../types"; +import { LOGIN, SERVICE_ACCOUNT_LOGIN, REFRESH } from "../common/api"; +import { + Context, + ConnectionOptions, + ServiceAccountAuth, + UsernamePasswordAuth, + AccessTokenAuth +} from "../types"; type Login = { access_token: string; @@ -8,12 +14,12 @@ type Login = { export class Authenticator { context: Context; - options: AuthOptions; + options: ConnectionOptions; accessToken?: string; refreshToken?: string; - constructor(context: Context, options: AuthOptions) { + constructor(context: Context, options: ConnectionOptions) { context.httpClient.authenticator = this; this.context = context; this.options = options; @@ -53,13 +59,14 @@ export class Authenticator { } } - async authenticate() { + authenticateWithToken(auth: AccessTokenAuth) { + const { accessToken } = auth; + this.accessToken = accessToken; + } + + async authenticateWithPassword(auth: UsernamePasswordAuth) { const { httpClient, apiEndpoint } = this.context; - const { username, password, accessToken } = this.options; - if (accessToken) { - this.accessToken = accessToken; - return; - } + const { username, password } = auth; const url = `${apiEndpoint}/${LOGIN}`; const body = JSON.stringify({ username, @@ -78,4 +85,55 @@ export class Authenticator { this.accessToken = access_token; this.refreshToken = refresh_token; } + + async authenticateServiceAccount(auth: ServiceAccountAuth) { + const { httpClient, apiEndpoint } = this.context; + const { client_id, client_secret } = auth; + + const params = new URLSearchParams({ + client_id, + client_secret, + grant_type: "client_credentials" + }); + const url = `${apiEndpoint}/${SERVICE_ACCOUNT_LOGIN}`; + + this.accessToken = undefined; + + const { access_token } = await httpClient + .request<{ access_token: string }>("POST", url, { + retry: false, + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: params + }) + .ready(); + + this.accessToken = access_token; + } + + async authenticate() { + const options = this.options.auth || this.options; + + if ((options as AccessTokenAuth).accessToken) { + this.authenticateWithToken(options as AccessTokenAuth); + return; + } + if ( + (options as UsernamePasswordAuth).username && + (options as UsernamePasswordAuth).password + ) { + await this.authenticateWithPassword(options as UsernamePasswordAuth); + return; + } + if ( + (options as ServiceAccountAuth).client_id && + (options as ServiceAccountAuth).client_secret + ) { + await this.authenticateServiceAccount(options as ServiceAccountAuth); + return; + } + + throw new Error("Please provide valid auth credentials"); + } } diff --git a/src/common/api.ts b/src/common/api.ts index 961d691b..280339e1 100644 --- a/src/common/api.ts +++ b/src/common/api.ts @@ -1,4 +1,5 @@ export const LOGIN = "auth/v1/login"; +export const SERVICE_ACCOUNT_LOGIN = "auth/v1/token"; export const REFRESH = "auth/v1/refresh"; export const DATABASES = "core/v1/account/databases"; diff --git a/src/common/errors.ts b/src/common/errors.ts index 8c855186..d417a1ec 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -1,3 +1,5 @@ +import { ConnectionOptions } from "../types"; + export const MISSING_USERNAME = 404001; export const MISSING_PASSWORD = 404002; export const MISSING_DATABASE = 404003; @@ -70,3 +72,18 @@ export class AuthenticationError extends Error { this.message = message; } } + +export const authDeprecationWarning = (options: ConnectionOptions) => { + if (!options.auth) { + console.error(` +username, password, accessToken fields are deprecated. +Please use auth object instead: +connectionOptions = { +auth: { +username: 'username', +password: 'password' +} +} +`); + } +}; diff --git a/src/core/index.ts b/src/core/index.ts index 1fd01f80..93a22a25 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,7 +2,11 @@ import { Connection } from "../connection"; import { Authenticator } from "../auth"; import { Context, ConnectionOptions, FireboltClientOptions } from "../types"; import { checkArgumentExists } from "../common/util"; -import { MISSING_PASSWORD, MISSING_USERNAME } from "../common/errors"; +import { + authDeprecationWarning, + MISSING_PASSWORD, + MISSING_USERNAME +} from "../common/errors"; import { ResourceManager } from "../service"; export class FireboltCore { @@ -17,10 +21,7 @@ export class FireboltCore { } checkConnectionOptions(connectionOptions: ConnectionOptions) { - if (!connectionOptions.accessToken) { - checkArgumentExists(connectionOptions.username, MISSING_USERNAME); - checkArgumentExists(connectionOptions.password, MISSING_PASSWORD); - } + authDeprecationWarning(connectionOptions); } async connect(connectionOptions: ConnectionOptions) { diff --git a/src/service/index.ts b/src/service/index.ts index 5278bfbb..ed7d7b34 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -6,6 +6,7 @@ import { AccountService } from "./account"; import { Authenticator } from "../auth"; import { QueryFormatter } from "../formatter"; import { AuthOptions, Context } from "../types"; +import { authDeprecationWarning } from "../common/errors"; export class ResourceManager { private context: Context; @@ -28,8 +29,9 @@ export class ResourceManager { this.account = new AccountService(this.context); } - async authenticate(options: AuthOptions & { account?: string }) { + async authenticate(options: { auth: AuthOptions; account?: string }) { const { account } = options; + authDeprecationWarning(options); const auth = new Authenticator(this.context, options); await auth.authenticate(); await this.account.resolveAccountId(account); diff --git a/src/types.ts b/src/types.ts index 55a0278f..8b549406 100644 --- a/src/types.ts +++ b/src/types.ts @@ -65,6 +65,25 @@ export type AdditionalConnectionParameters = { userClients?: ConnectorVersion[]; }; +export type UsernamePasswordAuth = { + username: string; + password: string; +}; + +export type AccessTokenAuth = { + accessToken: string; +}; + +export type ServiceAccountAuth = { + client_id: string; + client_secret: string; +}; + +export type AuthOptions = + | UsernamePasswordAuth + | AccessTokenAuth + | ServiceAccountAuth; + export type ConnectionOptions = { username?: string; password?: string; @@ -74,12 +93,7 @@ export type ConnectionOptions = { engineEndpoint?: string; additionalParameters?: AdditionalConnectionParameters; account?: string; -}; - -export type AuthOptions = { - username?: string; - password?: string; - accessToken?: string; + auth?: AuthOptions; }; export type FireboltClientOptions = { diff --git a/test/integration/engine.test.ts b/test/integration/engine.test.ts index 6e552b1c..b5dc16dc 100644 --- a/test/integration/engine.test.ts +++ b/test/integration/engine.test.ts @@ -102,7 +102,7 @@ describe("engine resource manager", () => { const resourceManager = FireboltResourceManager({ apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string }); - await resourceManager.authenticate(authOptions); + await resourceManager.authenticate({ auth: authOptions }); const engine = await resourceManager.engine.getByName( process.env.FIREBOLT_ENGINE_NAME as string ); diff --git a/test/integration/serviceAccounts.test.ts b/test/integration/serviceAccounts.test.ts new file mode 100644 index 00000000..2254fb60 --- /dev/null +++ b/test/integration/serviceAccounts.test.ts @@ -0,0 +1,39 @@ +import { Firebolt } from "../../src/index"; + +const connectionOptions = { + auth: { + client_id: process.env.FIREBOLT_CLIENT_ID as string, + client_secret: process.env.FIREBOLT_CLIENT_SECRET as string + }, + database: process.env.FIREBOLT_DATABASE as string, + engineName: process.env.FIREBOLT_ENGINE_NAME as string +}; + +describe("service accounts auth", () => { + it("retrieves a database by its name", async () => { + const firebolt = Firebolt({ + apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string + }); + + await firebolt.connect(connectionOptions); + + const { name } = await firebolt.resourceManager.database.getByName( + process.env.FIREBOLT_DATABASE as string + ); + + expect(name).toEqual(process.env.FIREBOLT_DATABASE); + }); + it("queries engine", async () => { + const firebolt = Firebolt({ + apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string + }); + + const connection = await firebolt.connect(connectionOptions); + + const statement = await connection.execute("SELECT 1"); + + const { data } = await statement.fetchResult(); + const row = data[0]; + expect(row).toMatchInlineSnapshot(); + }); +}); From a13d3b318af775c4c37625240e6949b2d1d7d67e Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 14 Dec 2022 15:16:47 +0200 Subject: [PATCH 2/3] remove unused code --- src/core/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index 93a22a25..e0cabf45 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,12 +1,7 @@ import { Connection } from "../connection"; import { Authenticator } from "../auth"; import { Context, ConnectionOptions, FireboltClientOptions } from "../types"; -import { checkArgumentExists } from "../common/util"; -import { - authDeprecationWarning, - MISSING_PASSWORD, - MISSING_USERNAME -} from "../common/errors"; +import { authDeprecationWarning } from "../common/errors"; import { ResourceManager } from "../service"; export class FireboltCore { From 116d3fac0b81196072c1e58ad4c6ae04fa82c2fe Mon Sep 17 00:00:00 2001 From: Vitalii Date: Wed, 14 Dec 2022 16:37:31 +0200 Subject: [PATCH 3/3] cleanup tests --- test/integration/account.test.ts | 6 ++-- test/integration/auth.test.ts | 37 ++++++++++++++++++++++++ test/integration/database.test.ts | 6 ++-- test/integration/engine.test.ts | 19 ++++++------ test/integration/index.test.ts | 24 ++++++++++----- test/integration/long.test.ts | 6 ++-- test/integration/outputFormat.test.ts | 6 ++-- test/integration/serviceAccounts.test.ts | 2 +- test/integration/stream.test.ts | 6 ++-- test/integration/systemEngine.test.ts | 6 ++-- test/unit/tracking.test.ts | 12 +++++--- 11 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 test/integration/auth.test.ts diff --git a/test/integration/account.test.ts b/test/integration/account.test.ts index cb66088f..26b57a9c 100644 --- a/test/integration/account.test.ts +++ b/test/integration/account.test.ts @@ -1,8 +1,10 @@ import { Firebolt } from "../../src/index"; const connectionOptions = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string, account: process.env.FIREBOLT_ACCOUNT as string diff --git a/test/integration/auth.test.ts b/test/integration/auth.test.ts new file mode 100644 index 00000000..4130ef5f --- /dev/null +++ b/test/integration/auth.test.ts @@ -0,0 +1,37 @@ +import { Firebolt } from "../../src/index"; + +const auth = { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string +}; + +const connectionOptions = { + database: process.env.FIREBOLT_DATABASE as string, + engineName: process.env.FIREBOLT_ENGINE_NAME as string, + account: process.env.FIREBOLT_ACCOUNT as string +}; + +jest.setTimeout(20000); + +describe("auth", () => { + it("support new auth connection options", async () => { + const firebolt = Firebolt({ + apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string + }); + + await firebolt.connect({ + ...connectionOptions, + auth + }); + }); + it("support old auth connection options", async () => { + const firebolt = Firebolt({ + apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string + }); + + await firebolt.connect({ + ...connectionOptions, + ...auth + }); + }); +}); diff --git a/test/integration/database.test.ts b/test/integration/database.test.ts index 75913566..34104e82 100644 --- a/test/integration/database.test.ts +++ b/test/integration/database.test.ts @@ -1,8 +1,10 @@ import { Firebolt } from "../../src/index"; const connectionOptions = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; diff --git a/test/integration/engine.test.ts b/test/integration/engine.test.ts index b5dc16dc..e0c7cd05 100644 --- a/test/integration/engine.test.ts +++ b/test/integration/engine.test.ts @@ -1,19 +1,18 @@ import { Firebolt, FireboltResourceManager } from "../../src/index"; -const connectionOptions = { +const authOptions = { username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + password: process.env.FIREBOLT_PASSWORD as string +}; + +const connectionOptions = { + auth: authOptions, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; jest.setTimeout(20000); -const authOptions = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string -}; - describe("engine integration", () => { it("starts engine", async () => { const firebolt = Firebolt({ @@ -58,8 +57,10 @@ describe("engine integration", () => { }); const connection = await firebolt.connect({ - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string }); diff --git a/test/integration/index.test.ts b/test/integration/index.test.ts index c11de363..22f53445 100644 --- a/test/integration/index.test.ts +++ b/test/integration/index.test.ts @@ -2,8 +2,10 @@ import { Firebolt } from "../../src/index"; import { OutputFormat } from "../../src/types"; const connectionParams = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; @@ -33,8 +35,10 @@ describe("integration test", () => { const connection2 = await firebolt.connect({ database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string, - // @ts-ignore - accessToken: connection.context.httpClient.authenticator.accessToken + auth: { + // @ts-ignore + accessToken: connection.context.httpClient.authenticator.accessToken + } }); const statement2 = await connection2.execute("SELECT 1"); @@ -90,8 +94,10 @@ describe("integration test", () => { }); const connection = await firebolt.connect({ - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineEndpoint: "bad engine url" }); @@ -177,8 +183,10 @@ describe("integration test", () => { await expect(async () => { await firebolt.testConnection({ - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: "unknown_engine" }); diff --git a/test/integration/long.test.ts b/test/integration/long.test.ts index cec03530..f64b76c0 100644 --- a/test/integration/long.test.ts +++ b/test/integration/long.test.ts @@ -1,8 +1,10 @@ import { Firebolt } from "../../src/index"; const connectionParams = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; diff --git a/test/integration/outputFormat.test.ts b/test/integration/outputFormat.test.ts index fb250703..567c8665 100644 --- a/test/integration/outputFormat.test.ts +++ b/test/integration/outputFormat.test.ts @@ -2,8 +2,10 @@ import { Firebolt } from "../../src/index"; import { OutputFormat } from "../../src/types"; const connectionParams = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; diff --git a/test/integration/serviceAccounts.test.ts b/test/integration/serviceAccounts.test.ts index 2254fb60..93a2e5f7 100644 --- a/test/integration/serviceAccounts.test.ts +++ b/test/integration/serviceAccounts.test.ts @@ -23,7 +23,7 @@ describe("service accounts auth", () => { expect(name).toEqual(process.env.FIREBOLT_DATABASE); }); - it("queries engine", async () => { + it.skip("queries engine", async () => { const firebolt = Firebolt({ apiEndpoint: process.env.FIREBOLT_API_ENDPOINT as string }); diff --git a/test/integration/stream.test.ts b/test/integration/stream.test.ts index 2acede37..f54f1b70 100644 --- a/test/integration/stream.test.ts +++ b/test/integration/stream.test.ts @@ -2,8 +2,10 @@ import stream, { TransformCallback } from "stream"; import { Firebolt } from "../../src/index"; const connectionParams = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, database: process.env.FIREBOLT_DATABASE as string, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; diff --git a/test/integration/systemEngine.test.ts b/test/integration/systemEngine.test.ts index 172722ff..a1800d61 100644 --- a/test/integration/systemEngine.test.ts +++ b/test/integration/systemEngine.test.ts @@ -1,8 +1,10 @@ import { Firebolt } from "../../src/index"; const connectionOptions = { - username: process.env.FIREBOLT_USERNAME as string, - password: process.env.FIREBOLT_PASSWORD as string, + auth: { + username: process.env.FIREBOLT_USERNAME as string, + password: process.env.FIREBOLT_PASSWORD as string + }, engineName: process.env.FIREBOLT_ENGINE_NAME as string }; diff --git a/test/unit/tracking.test.ts b/test/unit/tracking.test.ts index c7bae4d5..aee52ce6 100644 --- a/test/unit/tracking.test.ts +++ b/test/unit/tracking.test.ts @@ -60,8 +60,10 @@ describe("connection user agent", () => { it("propagation", async () => { const connectionParams: ConnectionOptions = { - username: "dummy", - password: "dummy", + auth: { + username: "dummy", + password: "dummy" + }, database: "dummy", engineName: "dummy", account: "account" @@ -83,8 +85,10 @@ describe("connection user agent", () => { }); it("customisation", async () => { const connectionParams: ConnectionOptions = { - username: "dummy", - password: "dummy", + auth: { + username: "dummy", + password: "dummy" + }, database: "dummy", engineName: "dummy", additionalParameters: {