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..e0cabf45 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -1,8 +1,7 @@
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 } from "../common/errors";
import { ResourceManager } from "../service";
export class FireboltCore {
@@ -17,10 +16,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/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 6e552b1c..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
});
@@ -102,7 +103,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/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
new file mode 100644
index 00000000..93a2e5f7
--- /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.skip("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();
+ });
+});
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: {