From 4bd1b29f6f59a89ff43d83e6b497ccebcecf3fbc Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Mon, 5 Jun 2023 11:09:21 +0800 Subject: [PATCH 1/7] feat: Canner PAT authenticator - add Canner PAT authenticator - add unit tests - add integration tests --- .../src/examples/example2-auth-user.spec.ts | 88 +++++-- ...xample5-get-user-profile-CannerPAT.spec.ts | 179 +++++++++++++ .../src/lib/auth/cannerPATAuthenticator.ts | 129 ++++++++++ packages/serve/src/lib/auth/index.ts | 3 + .../src/models/extensions/authenticator.ts | 3 +- .../test/auth/cannerPATAuthenticator.spec.ts | 237 ++++++++++++++++++ 6 files changed, 615 insertions(+), 24 deletions(-) create mode 100644 packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts create mode 100644 packages/serve/src/lib/auth/cannerPATAuthenticator.ts create mode 100644 packages/serve/test/auth/cannerPATAuthenticator.spec.ts diff --git a/packages/integration-testing/src/examples/example2-auth-user.spec.ts b/packages/integration-testing/src/examples/example2-auth-user.spec.ts index c16d64dd..e28094e5 100644 --- a/packages/integration-testing/src/examples/example2-auth-user.spec.ts +++ b/packages/integration-testing/src/examples/example2-auth-user.spec.ts @@ -2,10 +2,17 @@ import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build'; import { ServeConfig, VulcanServer } from '@vulcan-sql/serve'; import * as supertest from 'supertest'; import * as md5 from 'md5'; +import * as sinon from 'ts-sinon'; import defaultConfig from './projectConfig'; +import faker from '@faker-js/faker'; let server: VulcanServer; +// clear stub after each test +afterEach(() => { + sinon.default.restore(); +}); + const users = [ { name: 'william', @@ -23,29 +30,6 @@ const users = [ }, ]; -const projectConfig: ServeConfig & IBuildOptions = { - ...defaultConfig, - auth: { - enabled: true, - options: { - basic: { - 'users-list': [ - { - name: users[0].name, - md5Password: md5(users[0].password), - attr: users[0].attr, - }, - { - name: users[1].name, - md5Password: md5(users[1].password), - attr: users[1].attr, - }, - ], - }, - }, - }, -}; - afterEach(async () => { await server.close(); }); @@ -54,6 +38,28 @@ it.each([...users])( 'Example 2: authenticate user identity by POST /auth/token API', async ({ name, password }) => { // Arrange + const projectConfig: ServeConfig & IBuildOptions = { + ...defaultConfig, + auth: { + enabled: true, + options: { + basic: { + 'users-list': [ + { + name: users[0].name, + md5Password: md5(users[0].password), + attr: users[0].attr, + }, + { + name: users[1].name, + md5Password: md5(users[1].password), + attr: users[1].attr, + }, + ], + }, + }, + }, + }; const expected = Buffer.from(`${name}:${password}`).toString('base64'); const builder = new VulcanBuilder(projectConfig); await builder.build(); @@ -76,3 +82,39 @@ it.each([...users])( }, 10000 ); + +it('Example 2: authenticate user identity by POST /auth/token API using PAT should get 400', async () => { + // Arrange + const projectConfig: ServeConfig & IBuildOptions = { + ...defaultConfig, + auth: { + enabled: true, + options: { + 'canner-pat': { + host: 'mockhost', + port: faker.datatype.number({ min: 20000, max: 30000 }), + ssl: false, + }, + }, + }, + }; + const builder = new VulcanBuilder(projectConfig); + await builder.build(); + server = new VulcanServer(projectConfig); + const httpServer = (await server.start())['http']; + // Act + const agent = supertest(httpServer); + + // Assert + const result = await agent + .post('/auth/token') + .send({ + type: 'canner-pat', + }) + .set('Accept', 'application/json') + .set('Authorization', 'Canner-PAT mocktoken'); + expect(result.status).toBe(400); + expect(JSON.parse(result.text).message).toBe( + 'canner-pat does not support token generate.' + ); +}, 10000); diff --git a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts new file mode 100644 index 00000000..5d088103 --- /dev/null +++ b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts @@ -0,0 +1,179 @@ +import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build'; +import { + ServeConfig, + VulcanServer, + CannerPATAuthenticator, +} from '@vulcan-sql/serve'; +import * as supertest from 'supertest'; +import defaultConfig from './projectConfig'; +import * as sinon from 'ts-sinon'; + +describe('Example3: get user profile by GET /auth/user-profile API with Authorization', () => { + let server: VulcanServer; + let projectConfig: ServeConfig & IBuildOptions; + const mockUser = { + username: 'apple Hey', + firstName: 'Hey', + lastName: 'apple', + accountRole: 'admin', + + attributes: { + attr1: 100 * 10000, + attr2: 'Los Angeles', + }, + createdAt: '2023-03-27T12:48:15.882Z', + email: 'Alvina_Farrell82@yahoo.com', + groups: [{ id: 1, name: 'group1' }], + }; + const mockToken = `Canner-PAT myPATToken`; + const mockCannerUserResponse = { + status: 200, + data: { + data: { + userMe: mockUser, + }, + }, + }; + const expectedUserProfile = { + name: mockUser.username, + attr: { + firstName: 'Hey', + lastName: 'apple', + accountRole: 'admin', + + attributes: { + attr1: 100 * 10000, + attr2: 'Los Angeles', + }, + createdAt: '2023-03-27T12:48:15.882Z', + email: 'Alvina_Farrell82@yahoo.com', + groups: [{ id: 1, name: 'group1' }], + }, + }; + // stub fetchCannerUser method in class CannerPATAuthenticator using sinon + const stubFetchCannerUser = (user: any) => { + const stub = sinon.default.stub( + CannerPATAuthenticator.prototype, + 'fetchCannerUser' + ); + stub.resolves(user); + return stub; + }; + beforeEach(async () => { + projectConfig = { + ...defaultConfig, + auth: { + enabled: true, + options: { + 'canner-pat': { + host: 'mockhost', + port: 3000, + ssl: false, + }, + }, + }, + }; + }); + + afterEach(async () => { + sinon.default.restore(); + await server?.close(); + }); + + it('Example 3-1: set Authorization in header with default options', async () => { + stubFetchCannerUser(mockCannerUserResponse); + const builder = new VulcanBuilder(projectConfig); + await builder.build(); + server = new VulcanServer(projectConfig); + const httpServer = (await server.start())['http']; + + const agent = supertest(httpServer); + const result = await agent + .get('/auth/user-profile') + .set('Authorization', mockToken); + expect(result.body).toEqual(expectedUserProfile); + }, 10000); + + it('Example 3-2: set Authorization in querying with default options', async () => { + projectConfig['auth-source'] = { + options: { + key: 'x-auth', + }, + }; + stubFetchCannerUser(mockCannerUserResponse); + const builder = new VulcanBuilder(projectConfig); + await builder.build(); + server = new VulcanServer(projectConfig); + const httpServer = (await server.start())['http']; + + const agent = supertest(httpServer); + const auth = Buffer.from( + JSON.stringify({ + Authorization: `Canner-PAT ${Buffer.from(mockToken).toString( + 'base64' + )}`, + }) + ).toString('base64'); + const result = await agent.get(`/auth/user-profile?x-auth=${auth}`); + + expect(result.body).toEqual(expectedUserProfile); + }, 10000); + + it('Example 3-3: set Authorization in querying with specific auth "key" options', async () => { + projectConfig['auth-source'] = { + options: { + key: 'x-auth', + }, + }; + stubFetchCannerUser(mockCannerUserResponse); + const builder = new VulcanBuilder(projectConfig); + await builder.build(); + server = new VulcanServer(projectConfig); + const httpServer = (await server.start())['http']; + + const agent = supertest(httpServer); + const auth = Buffer.from( + JSON.stringify({ + Authorization: `Canner-PAT ${Buffer.from(mockToken).toString( + 'base64' + )}`, + }) + ).toString('base64'); + const result = await agent.get(`/auth/user-profile?x-auth=${auth}`); + + expect(result.body).toEqual(expectedUserProfile); + }, 10000); + + it('Example 3-4: set Authorization in json payload specific auth "x-key" options', async () => { + projectConfig['auth-source'] = { + options: { + key: 'x-auth', + in: 'payload', + }, + }; + stubFetchCannerUser(mockCannerUserResponse); + const builder = new VulcanBuilder(projectConfig); + await builder.build(); + server = new VulcanServer(projectConfig); + const httpServer = (await server.start())['http']; + + const auth = Buffer.from( + JSON.stringify({ + Authorization: `Canner-PAT ${Buffer.from(mockToken).toString( + 'base64' + )}`, + }) + ).toString('base64'); + + const agent = supertest(httpServer); + + const result = await agent + .get('/auth/user-profile') + .send({ + ['x-auth']: auth, + }) + .set('Accept', 'application/json'); + + expect(result.body).toEqual(expectedUserProfile); + }, 10000); +}); diff --git a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts b/packages/serve/src/lib/auth/cannerPATAuthenticator.ts new file mode 100644 index 00000000..1965bd31 --- /dev/null +++ b/packages/serve/src/lib/auth/cannerPATAuthenticator.ts @@ -0,0 +1,129 @@ +import { + UserError, + ConfigurationError, + VulcanExtensionId, + VulcanInternalExtension, + InternalError, +} from '@vulcan-sql/core'; +import { + BaseAuthenticator, + KoaContext, + AuthStatus, + AuthResult, + AuthType, +} from '@vulcan-sql/serve/models'; +import { isEmpty } from 'lodash'; +import * as axios from 'axios'; + +export interface CannerPATOptions { + host: string; + port: number; + // default is false + ssl: boolean; +} + +@VulcanInternalExtension('auth') +@VulcanExtensionId(AuthType.CannerPAT) +export class CannerPATAuthenticator extends BaseAuthenticator { + private options: CannerPATOptions = {} as CannerPATOptions; + + public override async onActivate() { + this.options = this.getOptions() as CannerPATOptions; + // const { host, port } = this.options; + // if (this.options && (!host || !port)) + // throw new ConfigurationError('please provide canner "host" and "port".'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async getTokenInfo(ctx: KoaContext): Promise { + throw new InternalError( + `${AuthType.CannerPAT} does not support token generate.` + ); + } + + public async authCredential(context: KoaContext) { + const incorrect = { + status: AuthStatus.INDETERMINATE, + type: this.getExtensionId()!, + }; + const authorize = context.request.headers['authorization']; + if ( + !authorize || + !authorize.toLowerCase().startsWith(this.getExtensionId()!) + ) + return incorrect; + + if (isEmpty(this.options) || !this.options.host || !this.options.port) + throw new ConfigurationError( + 'please provide correct connection information to Canner Enterprise, including "host" and "port".' + ); + + // validate request auth token + const token = authorize.trim().split(' ')[1]; + + try { + return await this.validate(token); + } catch (err) { + // if not found matched user credential, add WWW-Authenticate and return failed + context.set('WWW-Authenticate', this.getExtensionId()!); + return { + status: AuthStatus.FAIL, + type: this.getExtensionId()!, + message: (err as Error).message, + }; + } + } + + private async validate(token: string) { + const res = await this.fetchCannerUser(token); + if (res.status == 401) throw new UserError('invalid token'); + if (!res.data.data?.userMe) { + throw new InternalError('Can not retrieve user info from canner server'); + } + const cannerUser = res.data.data?.userMe; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { username, ...restAttrs } = cannerUser; + return { + status: AuthStatus.SUCCESS, + type: this.getExtensionId()!, // method name + user: { + name: cannerUser.username, + attr: restAttrs, + }, + } as AuthResult; + } + + private async fetchCannerUser(token: string) { + const graphqlUrl = this.getCannerUrl('/web/graphql'); + try { + return await axios.default.post( + graphqlUrl, + { + operationName: 'UserMe', + variables: {}, + query: + 'query UserMe{\n userMe {\n accountRole\n attributes\n createdAt\n email\n groups {\n id\n name\n }\n lastName\n firstName\n username\n }\n}', + }, + { + headers: { + Authorization: `Token ${token}`, + }, + } + ); + } catch (error) { + throw new InternalError( + `Failed to fetch user info from canner server: ${error}` + ); + } + } + private getCannerUrl = (path = '/') => { + const { host, port, ssl = false } = this.options; + if (process.env['IS_IN_K8S']) + return `http://${process.env['WEB_SERVICE_HOST']}${path}`; // for internal usage, we don't need to specify port + else { + const protocol = ssl ? 'https' : 'http'; + return `${protocol}://${host}:${port}${path}`; + } + }; +} diff --git a/packages/serve/src/lib/auth/index.ts b/packages/serve/src/lib/auth/index.ts index 988de126..47f4ed8f 100644 --- a/packages/serve/src/lib/auth/index.ts +++ b/packages/serve/src/lib/auth/index.ts @@ -1,13 +1,16 @@ export * from './simpleTokenAuthenticator'; export * from './passwordFileAuthenticator'; export * from './httpBasicAuthenticator'; +export * from './cannerPATAuthenticator'; import { SimpleTokenAuthenticator } from './simpleTokenAuthenticator'; import { PasswordFileAuthenticator } from './passwordFileAuthenticator'; import { BasicAuthenticator } from './httpBasicAuthenticator'; +import { CannerPATAuthenticator } from './cannerPATAuthenticator'; export const BuiltInAuthenticators = [ BasicAuthenticator, SimpleTokenAuthenticator, PasswordFileAuthenticator, + CannerPATAuthenticator, ]; diff --git a/packages/serve/src/models/extensions/authenticator.ts b/packages/serve/src/models/extensions/authenticator.ts index c1a4b0d8..6f501314 100644 --- a/packages/serve/src/models/extensions/authenticator.ts +++ b/packages/serve/src/models/extensions/authenticator.ts @@ -6,12 +6,13 @@ export enum AuthType { Basic = 'basic', PasswordFile = 'password-file', SimpleToken = 'simple-token', + CannerPAT = 'canner-pat', } export interface AuthUserInfo { name: string; attr: { - [field: string]: string | boolean | number; + [field: string]: string | boolean | number | any[]; }; } diff --git a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts b/packages/serve/test/auth/cannerPATAuthenticator.spec.ts new file mode 100644 index 00000000..eca4f934 --- /dev/null +++ b/packages/serve/test/auth/cannerPATAuthenticator.spec.ts @@ -0,0 +1,237 @@ +import * as sinon from 'ts-sinon'; +import { IncomingHttpHeaders } from 'http'; +import { Request, BaseResponse } from 'koa'; +import { CannerPATAuthenticator } from '@vulcan-sql/serve/auth'; +import { AuthResult, AuthStatus, KoaContext } from '@vulcan-sql/serve/models'; + +const wrappedAuthCredential = async ( + ctx: KoaContext, + options: any, + resolveValue: any = null +): Promise => { + const authenticator = new CannerPATAuthenticator({ options }, ''); + if (resolveValue) { + sinon.default + .stub(authenticator, 'fetchCannerUser') + .resolves(resolveValue); + } + await authenticator.activate(); + return await authenticator.authCredential(ctx); +}; + +const getTokenInfo = async ( + ctx: KoaContext, + options: any +): Promise> => { + const authenticator = new CannerPATAuthenticator({ options }, ''); + await authenticator.activate(); + return await authenticator.getTokenInfo(ctx); +}; + +const expectIncorrect = { + status: AuthStatus.INDETERMINATE, + type: 'canner-pat', +}; +const expectFailed = ( + message = 'authenticate user by "canner-pat" type failed.' +) => ({ + status: AuthStatus.FAIL, + type: 'canner-pat', + message, +}); +const mockOptions = { + 'canner-pat': { + host: 'mockHost', + port: 3000, + ssl: false, + }, +}; +const invalidToken = Buffer.from('clientId:clientSecret').toString('base64'); + +it.each([ + { 'canner-pat': { host: 'mockHost' } }, + { 'canner-pat': { port: 3000 } }, + { 'canner-pat': { ssl: false } }, + { 'canner-pat': {} }, +])('Should throw configuration error when options = %p', async (options) => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + authorization: 'Canner-PAT 1234567890', + }, + }, + } as KoaContext; + + // Act, Assert + await expect(wrappedAuthCredential(ctx, options)).rejects.toThrow( + 'please provide correct connection information to Canner Enterprise, including "host" and "port".' + ); +}); +it('Test to auth credential failed when request header not exist "authorization" key', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + ...sinon.stubInterface(), + }, + }, + } as KoaContext; + + // Act + const result = await wrappedAuthCredential(ctx, mockOptions); + + // Assert + expect(result).toEqual(expectIncorrect); +}); + +it('Should auth credential failed when request header "authorization" not start with "canner-pat"', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + ...sinon.stubInterface(), + authorization: 'Incorrect-Prefix 1234567890', + }, + }, + } as KoaContext; + + // Act + const result = await wrappedAuthCredential(ctx, mockOptions); + + // Assert + expect(result).toEqual(expectIncorrect); +}); + +it('Should auth credential failed when the PAT token in the authorization header does not pass the canner host', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + ...sinon.stubInterface(), + authorization: `Canner-PAT ${invalidToken}`, + }, + }, + set: sinon.stubInterface().set, + }; + // Act + const mockResolveValue = { + status: 401, + data: { error: 'invalid token from canner' }, + }; + const result = await wrappedAuthCredential( + ctx, + mockOptions, + mockResolveValue + ); + + // Assert + expect(result).toEqual(expectFailed('invalid token')); +}); + +it('Should auth credential failed when the canner host does not return the userMe', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + ...sinon.stubInterface(), + authorization: `Canner-PAT ${invalidToken}`, + }, + }, + set: sinon.stubInterface().set, + }; + const mockResolveValue = { + status: 200, + // the expected response data should be { data: { userMe: { attrs... } } } + data: { data: { notUserMe: null } }, + }; + + // Act + const result = await wrappedAuthCredential( + ctx, + mockOptions, + mockResolveValue + ); + + // Assert + expect(result).toEqual( + expectFailed('Can not retrieve user info from canner server') + ); +}); + +it('Should auth credential successful when request header "authorization" pass the canner host', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + ...sinon.stubInterface(), + authorization: `Canner-PAT eyValidToken`, + }, + }, + } as KoaContext; + const mockResolveValue = { + status: 200, + data: { + data: { + userMe: { + email: 'myEmail@google.com', + enabled: true, + username: 'mockUser', + attr1: 'value1', + }, + }, + }, + }; + + const expected = { + status: AuthStatus.SUCCESS, + type: 'canner-pat', + user: { + name: 'mockUser', + attr: { + email: 'myEmail@google.com', + enabled: true, + attr1: 'value1', + }, + }, + } as AuthResult; + + // Act + const result = await wrappedAuthCredential( + ctx, + mockOptions, + mockResolveValue + ); + + // Assert + expect(result).toEqual(expected); +}); + +// Token info +it('Should throw when use getTokenInfo with cannerPATAuthenticator', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + body: {}, + }, + }; + + // Act Assert + await expect(getTokenInfo(ctx, mockOptions)).rejects.toThrow( + 'canner-pat does not support token generate.' + ); +}); From 824d13393ed743f6ddaf8613e920d704b3067b90 Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Wed, 7 Jun 2023 16:56:40 +0800 Subject: [PATCH 2/7] chore: remove unused code, handle axios error, add test case to test axios error handling --- .../test/cannerDataSource.spec.ts | 1 + .../src/examples/example2-auth-user.spec.ts | 6 - ...xample5-get-user-profile-CannerPAT.spec.ts | 2 +- .../src/lib/auth/cannerPATAuthenticator.ts | 22 ++-- .../test/auth/cannerPATAuthenticator.spec.ts | 106 ++++++++++++------ 5 files changed, 86 insertions(+), 51 deletions(-) diff --git a/packages/extension-driver-canner/test/cannerDataSource.spec.ts b/packages/extension-driver-canner/test/cannerDataSource.spec.ts index ac9a7ebb..18d30ba6 100644 --- a/packages/extension-driver-canner/test/cannerDataSource.spec.ts +++ b/packages/extension-driver-canner/test/cannerDataSource.spec.ts @@ -62,6 +62,7 @@ it('Data source should export successfully', async () => { it('Data source should throw when fail to export data', async () => { // Arrange + // TODO: refactor to avoid stubbing private method // stub the private function to manipulate getting error from the remote server sinon.default .stub(CannerAdapter.prototype, 'createAsyncQueryResultUrls') diff --git a/packages/integration-testing/src/examples/example2-auth-user.spec.ts b/packages/integration-testing/src/examples/example2-auth-user.spec.ts index e28094e5..a7aa9ae5 100644 --- a/packages/integration-testing/src/examples/example2-auth-user.spec.ts +++ b/packages/integration-testing/src/examples/example2-auth-user.spec.ts @@ -2,17 +2,11 @@ import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build'; import { ServeConfig, VulcanServer } from '@vulcan-sql/serve'; import * as supertest from 'supertest'; import * as md5 from 'md5'; -import * as sinon from 'ts-sinon'; import defaultConfig from './projectConfig'; import faker from '@faker-js/faker'; let server: VulcanServer; -// clear stub after each test -afterEach(() => { - sinon.default.restore(); -}); - const users = [ { name: 'william', diff --git a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts index 5d088103..9c850d1a 100644 --- a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts +++ b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts @@ -50,7 +50,7 @@ describe('Example3: get user profile by GET /auth/user-profile API with Authoriz groups: [{ id: 1, name: 'group1' }], }, }; - // stub fetchCannerUser method in class CannerPATAuthenticator using sinon + // stub the private function to manipulate getting user info from remote server const stubFetchCannerUser = (user: any) => { const stub = sinon.default.stub( CannerPATAuthenticator.prototype, diff --git a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts b/packages/serve/src/lib/auth/cannerPATAuthenticator.ts index 1965bd31..362c0a33 100644 --- a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts +++ b/packages/serve/src/lib/auth/cannerPATAuthenticator.ts @@ -13,7 +13,7 @@ import { AuthType, } from '@vulcan-sql/serve/models'; import { isEmpty } from 'lodash'; -import * as axios from 'axios'; +import axios from 'axios'; export interface CannerPATOptions { host: string; @@ -29,9 +29,6 @@ export class CannerPATAuthenticator extends BaseAuthenticator public override async onActivate() { this.options = this.getOptions() as CannerPATOptions; - // const { host, port } = this.options; - // if (this.options && (!host || !port)) - // throw new ConfigurationError('please provide canner "host" and "port".'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -64,8 +61,6 @@ export class CannerPATAuthenticator extends BaseAuthenticator try { return await this.validate(token); } catch (err) { - // if not found matched user credential, add WWW-Authenticate and return failed - context.set('WWW-Authenticate', this.getExtensionId()!); return { status: AuthStatus.FAIL, type: this.getExtensionId()!, @@ -97,13 +92,13 @@ export class CannerPATAuthenticator extends BaseAuthenticator private async fetchCannerUser(token: string) { const graphqlUrl = this.getCannerUrl('/web/graphql'); try { - return await axios.default.post( + return await axios.post( graphqlUrl, { operationName: 'UserMe', variables: {}, query: - 'query UserMe{\n userMe {\n accountRole\n attributes\n createdAt\n email\n groups {\n id\n name\n }\n lastName\n firstName\n username\n }\n}', + 'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username', }, { headers: { @@ -111,15 +106,20 @@ export class CannerPATAuthenticator extends BaseAuthenticator }, } ); - } catch (error) { + } catch (error: any) { + const message = error.response + ? `response status: ${ + error.response.status + }, response data: ${JSON.stringify(error.response.data)}` + : `remote server does not response. request ${error.toJSON()}}`; throw new InternalError( - `Failed to fetch user info from canner server: ${error}` + `Failed to fetch user info from canner server: ${message}` ); } } private getCannerUrl = (path = '/') => { const { host, port, ssl = false } = this.options; - if (process.env['IS_IN_K8S']) + if (process.env['IS_ON_KUBERNETES']) return `http://${process.env['WEB_SERVICE_HOST']}${path}`; // for internal usage, we don't need to specify port else { const protocol = ssl ? 'https' : 'http'; diff --git a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts b/packages/serve/test/auth/cannerPATAuthenticator.spec.ts index eca4f934..e3bd35d5 100644 --- a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts +++ b/packages/serve/test/auth/cannerPATAuthenticator.spec.ts @@ -1,31 +1,27 @@ import * as sinon from 'ts-sinon'; +import axios from 'axios'; import { IncomingHttpHeaders } from 'http'; import { Request, BaseResponse } from 'koa'; import { CannerPATAuthenticator } from '@vulcan-sql/serve/auth'; import { AuthResult, AuthStatus, KoaContext } from '@vulcan-sql/serve/models'; -const wrappedAuthCredential = async ( - ctx: KoaContext, +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const getStubAuthenticator = async ( options: any, - resolveValue: any = null -): Promise => { + stubValue: any = null +): Promise => { const authenticator = new CannerPATAuthenticator({ options }, ''); - if (resolveValue) { + if (stubValue) { + // TODO: refactor to avoid stubbing private method + // stub the private method fetchCannerUser to simulate the response from remote server sinon.default .stub(authenticator, 'fetchCannerUser') - .resolves(resolveValue); + .resolves(stubValue); } await authenticator.activate(); - return await authenticator.authCredential(ctx); -}; - -const getTokenInfo = async ( - ctx: KoaContext, - options: any -): Promise> => { - const authenticator = new CannerPATAuthenticator({ options }, ''); - await authenticator.activate(); - return await authenticator.getTokenInfo(ctx); + return authenticator; }; const expectIncorrect = { @@ -64,12 +60,54 @@ it.each([ }, }, } as KoaContext; + const authenticator = await getStubAuthenticator(options); // Act, Assert - await expect(wrappedAuthCredential(ctx, options)).rejects.toThrow( + await expect(authenticator.authCredential(ctx)).rejects.toThrow( 'please provide correct connection information to Canner Enterprise, including "host" and "port".' ); }); + +it('Should throw error when catch error when fetching the remote server for user info', async () => { + // Arrange + const ctx = { + ...sinon.stubInterface(), + request: { + ...sinon.stubInterface(), + headers: { + ...sinon.stubInterface(), + authorization: `Canner-PAT eyValidToken`, + }, + }, + } as KoaContext; + // mock axios.post method get non 200 response from remote server then axios throw error + mockedAxios.post.mockRejectedValue({ + request: 'mock request', + response: { + data: { error: 'mock response error' }, + status: 500, + statusText: 'string', + headers: {}, + config: {}, + } as any, + isAxiosError: true, + toJSON: () => ({}), + message: 'An error occurred!', + name: 'Error', + }); + const authenticator = await getStubAuthenticator(mockOptions); + + // Act + const res = await authenticator.authCredential(ctx); + + // Assert + await expect(res).toEqual( + expectFailed( + 'Failed to fetch user info from canner server: response status: 500, response data: {"error":"mock response error"}' + ) + ); +}, 10000); + it('Test to auth credential failed when request header not exist "authorization" key', async () => { // Arrange const ctx = { @@ -81,9 +119,10 @@ it('Test to auth credential failed when request header not exist "authorization" }, }, } as KoaContext; + const authenticator = await getStubAuthenticator(mockOptions); // Act - const result = await wrappedAuthCredential(ctx, mockOptions); + const result = await authenticator.authCredential(ctx); // Assert expect(result).toEqual(expectIncorrect); @@ -101,9 +140,10 @@ it('Should auth credential failed when request header "authorization" not start }, }, } as KoaContext; + const authenticator = await getStubAuthenticator(mockOptions); // Act - const result = await wrappedAuthCredential(ctx, mockOptions); + const result = await authenticator.authCredential(ctx); // Assert expect(result).toEqual(expectIncorrect); @@ -122,17 +162,18 @@ it('Should auth credential failed when the PAT token in the authorization header }, set: sinon.stubInterface().set, }; - // Act const mockResolveValue = { status: 401, data: { error: 'invalid token from canner' }, }; - const result = await wrappedAuthCredential( - ctx, + const authenticator = await getStubAuthenticator( mockOptions, mockResolveValue ); + // Act + const result = await authenticator.authCredential(ctx); + // Assert expect(result).toEqual(expectFailed('invalid token')); }); @@ -155,14 +196,14 @@ it('Should auth credential failed when the canner host does not return the userM // the expected response data should be { data: { userMe: { attrs... } } } data: { data: { notUserMe: null } }, }; - - // Act - const result = await wrappedAuthCredential( - ctx, + const authenticator = await getStubAuthenticator( mockOptions, mockResolveValue ); + // Act + const result = await authenticator.authCredential(ctx); + // Assert expect(result).toEqual( expectFailed('Can not retrieve user info from canner server') @@ -194,7 +235,6 @@ it('Should auth credential successful when request header "authorization" pass t }, }, }; - const expected = { status: AuthStatus.SUCCESS, type: 'canner-pat', @@ -207,14 +247,14 @@ it('Should auth credential successful when request header "authorization" pass t }, }, } as AuthResult; - - // Act - const result = await wrappedAuthCredential( - ctx, + const authenticator = await getStubAuthenticator( mockOptions, mockResolveValue ); + // Act + const result = await authenticator.authCredential(ctx); + // Assert expect(result).toEqual(expected); }); @@ -229,9 +269,9 @@ it('Should throw when use getTokenInfo with cannerPATAuthenticator', async () => body: {}, }, }; - + const authenticator = await getStubAuthenticator(mockOptions); // Act Assert - await expect(getTokenInfo(ctx, mockOptions)).rejects.toThrow( + await expect(authenticator.getTokenInfo(ctx)).rejects.toThrow( 'canner-pat does not support token generate.' ); }); From 08aa72d60f3c92f4cfacdc0889c24ce31f48c7fe Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Thu, 8 Jun 2023 12:44:59 +0800 Subject: [PATCH 3/7] fix(feature/authenticator): fix incorrect query structure, remove unused code and refine test cases --- ...xample5-get-user-profile-CannerPAT.spec.ts | 1 + .../src/lib/auth/cannerPATAuthenticator.ts | 14 +-- .../test/auth/cannerPATAuthenticator.spec.ts | 115 ++++-------------- 3 files changed, 32 insertions(+), 98 deletions(-) diff --git a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts index 9c850d1a..e4b8559f 100644 --- a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts +++ b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts @@ -50,6 +50,7 @@ describe('Example3: get user profile by GET /auth/user-profile API with Authoriz groups: [{ id: 1, name: 'group1' }], }, }; + // TODO: refactor to avoid stubbing private method // stub the private function to manipulate getting user info from remote server const stubFetchCannerUser = (user: any) => { const stub = sinon.default.stub( diff --git a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts b/packages/serve/src/lib/auth/cannerPATAuthenticator.ts index 362c0a33..8020f6d0 100644 --- a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts +++ b/packages/serve/src/lib/auth/cannerPATAuthenticator.ts @@ -71,19 +71,13 @@ export class CannerPATAuthenticator extends BaseAuthenticator private async validate(token: string) { const res = await this.fetchCannerUser(token); - if (res.status == 401) throw new UserError('invalid token'); - if (!res.data.data?.userMe) { - throw new InternalError('Can not retrieve user info from canner server'); - } const cannerUser = res.data.data?.userMe; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { username, ...restAttrs } = cannerUser; return { status: AuthStatus.SUCCESS, type: this.getExtensionId()!, // method name user: { - name: cannerUser.username, + name: username, attr: restAttrs, }, } as AuthResult; @@ -98,7 +92,7 @@ export class CannerPATAuthenticator extends BaseAuthenticator operationName: 'UserMe', variables: {}, query: - 'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username', + 'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username}', }, { headers: { @@ -117,7 +111,7 @@ export class CannerPATAuthenticator extends BaseAuthenticator ); } } - private getCannerUrl = (path = '/') => { + private getCannerUrl(path = '/') { const { host, port, ssl = false } = this.options; if (process.env['IS_ON_KUBERNETES']) return `http://${process.env['WEB_SERVICE_HOST']}${path}`; // for internal usage, we don't need to specify port @@ -125,5 +119,5 @@ export class CannerPATAuthenticator extends BaseAuthenticator const protocol = ssl ? 'https' : 'http'; return `${protocol}://${host}:${port}${path}`; } - }; + } } diff --git a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts b/packages/serve/test/auth/cannerPATAuthenticator.spec.ts index e3bd35d5..7edfb047 100644 --- a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts +++ b/packages/serve/test/auth/cannerPATAuthenticator.spec.ts @@ -1,7 +1,7 @@ import * as sinon from 'ts-sinon'; import axios from 'axios'; import { IncomingHttpHeaders } from 'http'; -import { Request, BaseResponse } from 'koa'; +import { Request } from 'koa'; import { CannerPATAuthenticator } from '@vulcan-sql/serve/auth'; import { AuthResult, AuthStatus, KoaContext } from '@vulcan-sql/serve/models'; @@ -42,7 +42,6 @@ const mockOptions = { ssl: false, }, }; -const invalidToken = Buffer.from('clientId:clientSecret').toString('base64'); it.each([ { 'canner-pat': { host: 'mockHost' } }, @@ -68,46 +67,6 @@ it.each([ ); }); -it('Should throw error when catch error when fetching the remote server for user info', async () => { - // Arrange - const ctx = { - ...sinon.stubInterface(), - request: { - ...sinon.stubInterface(), - headers: { - ...sinon.stubInterface(), - authorization: `Canner-PAT eyValidToken`, - }, - }, - } as KoaContext; - // mock axios.post method get non 200 response from remote server then axios throw error - mockedAxios.post.mockRejectedValue({ - request: 'mock request', - response: { - data: { error: 'mock response error' }, - status: 500, - statusText: 'string', - headers: {}, - config: {}, - } as any, - isAxiosError: true, - toJSON: () => ({}), - message: 'An error occurred!', - name: 'Error', - }); - const authenticator = await getStubAuthenticator(mockOptions); - - // Act - const res = await authenticator.authCredential(ctx); - - // Assert - await expect(res).toEqual( - expectFailed( - 'Failed to fetch user info from canner server: response status: 500, response data: {"error":"mock response error"}' - ) - ); -}, 10000); - it('Test to auth credential failed when request header not exist "authorization" key', async () => { // Arrange const ctx = { @@ -149,36 +108,8 @@ it('Should auth credential failed when request header "authorization" not start expect(result).toEqual(expectIncorrect); }); -it('Should auth credential failed when the PAT token in the authorization header does not pass the canner host', async () => { - // Arrange - const ctx = { - ...sinon.stubInterface(), - request: { - ...sinon.stubInterface(), - headers: { - ...sinon.stubInterface(), - authorization: `Canner-PAT ${invalidToken}`, - }, - }, - set: sinon.stubInterface().set, - }; - const mockResolveValue = { - status: 401, - data: { error: 'invalid token from canner' }, - }; - const authenticator = await getStubAuthenticator( - mockOptions, - mockResolveValue - ); - - // Act - const result = await authenticator.authCredential(ctx); - - // Assert - expect(result).toEqual(expectFailed('invalid token')); -}); - -it('Should auth credential failed when the canner host does not return the userMe', async () => { +// the situation of status code 4xx, 5xx(including 401, 403) is handled by axios +it('Should auth credential failed when getting status code that is not 2xx from the remote server', async () => { // Arrange const ctx = { ...sinon.stubInterface(), @@ -186,29 +117,37 @@ it('Should auth credential failed when the canner host does not return the userM ...sinon.stubInterface(), headers: { ...sinon.stubInterface(), - authorization: `Canner-PAT ${invalidToken}`, + authorization: `Canner-PAT eyValidToken`, }, }, - set: sinon.stubInterface().set, - }; - const mockResolveValue = { - status: 200, - // the expected response data should be { data: { userMe: { attrs... } } } - data: { data: { notUserMe: null } }, - }; - const authenticator = await getStubAuthenticator( - mockOptions, - mockResolveValue - ); + } as KoaContext; + // mock axios.post method get non 200 response from remote server then axios throw error + mockedAxios.post.mockRejectedValue({ + request: 'mock request', + response: { + data: { error: 'mock response error' }, + status: 500, + statusText: 'string', + headers: {}, + config: {}, + } as any, + isAxiosError: true, + toJSON: () => ({}), + message: 'An error occurred!', + name: 'Error', + }); + const authenticator = await getStubAuthenticator(mockOptions); // Act - const result = await authenticator.authCredential(ctx); + const res = await authenticator.authCredential(ctx); // Assert - expect(result).toEqual( - expectFailed('Can not retrieve user info from canner server') + await expect(res).toEqual( + expectFailed( + 'Failed to fetch user info from canner server: response status: 500, response data: {"error":"mock response error"}' + ) ); -}); +}, 10000); it('Should auth credential successful when request header "authorization" pass the canner host', async () => { // Arrange From efdacb0ef7d885b75b6b5b604dec2c50bba3f8f7 Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Mon, 12 Jun 2023 14:59:50 +0800 Subject: [PATCH 4/7] feat(feature/authenticator): move canner pat authenticator from serve module to extension --- labs/playground1/Makefile | 9 +- .../.eslintrc.json | 18 ++ .../extension-authenticator-canner/README.md | 46 +++++ .../jest.config.ts | 14 ++ .../package.json | 30 +++ .../project.json | 32 ++++ .../src/index.ts | 3 + .../src/lib/authenticator/index.ts | 1 + .../src/lib/authenticator/pat.ts} | 25 ++- .../src/lib/config.ts | 10 + .../src/lib/index.ts | 1 + .../src/test}/cannerPATAuthenticator.spec.ts | 6 +- .../tsconfig.json | 22 +++ .../tsconfig.lib.json | 10 + .../tsconfig.spec.json | 9 + .../src/examples/example2-auth-user.spec.ts | 37 ---- ...xample5-get-user-profile-CannerPAT.spec.ts | 180 ------------------ .../src/lib/auth/httpBasicAuthenticator.ts | 1 + packages/serve/src/lib/auth/index.ts | 3 - .../src/lib/auth/passwordFileAuthenticator.ts | 1 + .../auth/authCredentialsMiddleware.ts | 2 - .../src/lib/middleware/auth/authMiddleware.ts | 19 +- .../src/models/extensions/authenticator.ts | 1 - tsconfig.base.json | 9 +- workspace.json | 3 +- 25 files changed, 236 insertions(+), 256 deletions(-) create mode 100644 packages/extension-authenticator-canner/.eslintrc.json create mode 100644 packages/extension-authenticator-canner/README.md create mode 100644 packages/extension-authenticator-canner/jest.config.ts create mode 100644 packages/extension-authenticator-canner/package.json create mode 100644 packages/extension-authenticator-canner/project.json create mode 100644 packages/extension-authenticator-canner/src/index.ts create mode 100644 packages/extension-authenticator-canner/src/lib/authenticator/index.ts rename packages/{serve/src/lib/auth/cannerPATAuthenticator.ts => extension-authenticator-canner/src/lib/authenticator/pat.ts} (83%) create mode 100644 packages/extension-authenticator-canner/src/lib/config.ts create mode 100644 packages/extension-authenticator-canner/src/lib/index.ts rename packages/{serve/test/auth => extension-authenticator-canner/src/test}/cannerPATAuthenticator.spec.ts (97%) create mode 100644 packages/extension-authenticator-canner/tsconfig.json create mode 100644 packages/extension-authenticator-canner/tsconfig.lib.json create mode 100644 packages/extension-authenticator-canner/tsconfig.spec.json delete mode 100644 packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts diff --git a/labs/playground1/Makefile b/labs/playground1/Makefile index 850576c5..f1cc0eb4 100644 --- a/labs/playground1/Makefile +++ b/labs/playground1/Makefile @@ -6,7 +6,7 @@ start: build test-data/moma.db ../../node_modules @vulcan start # build the required packages -build: pkg-core pkg-build pkg-serve pkg-catalog-server pkg-cli pkg-extension-driver-duckdb +build: pkg-core pkg-build pkg-serve pkg-catalog-server pkg-cli pkg-extension-driver-duckdb pkg-extension-authenticator-canner # build for core pakge @@ -49,6 +49,13 @@ pkg-extension-driver-duckdb: ../../node_modules rm -rf ./labs/playground1/node_modules/@vulcan-sql/extension-driver-duckdb; \ cp -R ./dist/packages/extension-driver-duckdb ./labs/playground1/node_modules/@vulcan-sql +pkg-extension-authenticator-canner: ../../node_modules + @cd ../..; \ + yarn nx build extension-authenticator-canner; \ + mkdir -p ./labs/playground1/node_modules/@vulcan-sql; \ + rm -rf ./labs/playground1/node_modules/@vulcan-sql/extension-authenticator-canner; \ + cp -R ./dist/packages/extension-authenticator-canner ./labs/playground1/node_modules/@vulcan-sql + # build and install for cli pakge pkg-cli: ../../node_modules @cd ../..; \ diff --git a/packages/extension-authenticator-canner/.eslintrc.json b/packages/extension-authenticator-canner/.eslintrc.json new file mode 100644 index 00000000..9d9c0db5 --- /dev/null +++ b/packages/extension-authenticator-canner/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/extension-authenticator-canner/README.md b/packages/extension-authenticator-canner/README.md new file mode 100644 index 00000000..4afe3116 --- /dev/null +++ b/packages/extension-authenticator-canner/README.md @@ -0,0 +1,46 @@ +# extension-authenticator-canner + +This extension make Data API(VulcanSQL API) can integrate with [Canner Enterprise](https://cannerdata.com/product) and use Canner as a authenticate server + +This extension let Data API request can be authenticated with [Canner PAT](https://docs.cannerdata.com/product/api_sdk/api_personal_access_token) + +## Install + +1. Install package + + ```sql + npm i @vulcan-sql/extension-authenticator-canner + ``` + +2. Update `vulcan.yaml`, enable the extension and enable the `auth` configuration. + + ```yaml + auth: + enabled: true + # The extension-authenticator-canner and [build-in authenticator](https://vulcansql.com/docs/data-privacy/authn) can work at the same time + + extensions: + canner-authenticator: '@vulcan-sql/extension-authenticator-canner' + ``` + +3. Update `vulcan.yaml`, define your `canner-authenticator` + ```yaml + canner-authenticator: + # To having the same config structure to the authenticator middleware, we + options: + canner-pat: + # your canner enterprise host + host: 'my-canner-host-dns' + # your canner enterprise post + post: 443 + # indicate using http or https default is false + ssl: true + ``` + +## Testing + +```bash +nx test extension-authenticator-canner +``` + +This library was generated with [Nx](https://nx.dev). diff --git a/packages/extension-authenticator-canner/jest.config.ts b/packages/extension-authenticator-canner/jest.config.ts new file mode 100644 index 00000000..c7334446 --- /dev/null +++ b/packages/extension-authenticator-canner/jest.config.ts @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'extension-authenticator-canner', + preset: '../../jest.preset.ts', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/extension-authenticator-canner', +}; diff --git a/packages/extension-authenticator-canner/package.json b/packages/extension-authenticator-canner/package.json new file mode 100644 index 00000000..f46c6be5 --- /dev/null +++ b/packages/extension-authenticator-canner/package.json @@ -0,0 +1,30 @@ +{ + "name": "@vulcan-sql/extension-authenticator-canner", + "description": "Canner Enterprise authenticator for Vulcan SQL", + "version": "0.4.0", + "type": "commonjs", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "vulcan", + "vulcan-sql", + "data", + "sql", + "database", + "data-warehouse", + "data-lake", + "api-builder", + "postgres", + "pg" + ], + "repository": { + "type": "git", + "url": "https://github.com/Canner/vulcan.git" + }, + "license": "MIT", + "peerDependencies": { + "@vulcan-sql/core": "~0.4.0-0", + "@vulcan-sql/serve": "~0.4.0-0" + } +} diff --git a/packages/extension-authenticator-canner/project.json b/packages/extension-authenticator-canner/project.json new file mode 100644 index 00000000..e230c7e1 --- /dev/null +++ b/packages/extension-authenticator-canner/project.json @@ -0,0 +1,32 @@ +{ + "root": "packages/extension-authenticator-canner", + "sourceRoot": "packages/extension-authenticator-canner/src", + "targets": { + "build": { + "executor": "@nrwl/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/extension-authenticator-canner", + "main": "packages/extension-authenticator-canner/src/index.ts", + "tsConfig": "packages/extension-authenticator-canner/tsconfig.lib.json", + "assets": ["packages/extension-authenticator-canner/*.md"] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/extension-authenticator-canner/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/packages/extension-authenticator-canner"], + "options": { + "jestConfig": "packages/extension-authenticator-canner/jest.config.ts", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/packages/extension-authenticator-canner/src/index.ts b/packages/extension-authenticator-canner/src/index.ts new file mode 100644 index 00000000..69e11325 --- /dev/null +++ b/packages/extension-authenticator-canner/src/index.ts @@ -0,0 +1,3 @@ +import { CannerPATAuthenticator } from './lib'; + +export default [CannerPATAuthenticator]; diff --git a/packages/extension-authenticator-canner/src/lib/authenticator/index.ts b/packages/extension-authenticator-canner/src/lib/authenticator/index.ts new file mode 100644 index 00000000..91d4d1ac --- /dev/null +++ b/packages/extension-authenticator-canner/src/lib/authenticator/index.ts @@ -0,0 +1 @@ +export * from './pat'; diff --git a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts b/packages/extension-authenticator-canner/src/lib/authenticator/pat.ts similarity index 83% rename from packages/serve/src/lib/auth/cannerPATAuthenticator.ts rename to packages/extension-authenticator-canner/src/lib/authenticator/pat.ts index 8020f6d0..b8b4dd7f 100644 --- a/packages/serve/src/lib/auth/cannerPATAuthenticator.ts +++ b/packages/extension-authenticator-canner/src/lib/authenticator/pat.ts @@ -1,8 +1,6 @@ import { - UserError, ConfigurationError, VulcanExtensionId, - VulcanInternalExtension, InternalError, } from '@vulcan-sql/core'; import { @@ -10,10 +8,10 @@ import { KoaContext, AuthStatus, AuthResult, - AuthType, -} from '@vulcan-sql/serve/models'; +} from '@vulcan-sql/serve'; import { isEmpty } from 'lodash'; import axios from 'axios'; +import config from '../config'; export interface CannerPATOptions { host: string; @@ -22,8 +20,7 @@ export interface CannerPATOptions { ssl: boolean; } -@VulcanInternalExtension('auth') -@VulcanExtensionId(AuthType.CannerPAT) +@VulcanExtensionId('canner-pat') export class CannerPATAuthenticator extends BaseAuthenticator { private options: CannerPATOptions = {} as CannerPATOptions; @@ -33,9 +30,7 @@ export class CannerPATAuthenticator extends BaseAuthenticator // eslint-disable-next-line @typescript-eslint/no-unused-vars public async getTokenInfo(ctx: KoaContext): Promise { - throw new InternalError( - `${AuthType.CannerPAT} does not support token generate.` - ); + throw new InternalError(`canner-pat does not support token generate.`); } public async authCredential(context: KoaContext) { @@ -45,14 +40,16 @@ export class CannerPATAuthenticator extends BaseAuthenticator }; const authorize = context.request.headers['authorization']; if ( + // no need to check this.options because it a external extension, + // it must be configured correctly to be load into container and can be used in authenticate middleware !authorize || !authorize.toLowerCase().startsWith(this.getExtensionId()!) ) return incorrect; - if (isEmpty(this.options) || !this.options.host || !this.options.port) + if (isEmpty(this.options) || !this.options.host) throw new ConfigurationError( - 'please provide correct connection information to Canner Enterprise, including "host" and "port".' + 'please provide correct connection information to Canner Enterprise, including "host".' ); // validate request auth token @@ -92,7 +89,7 @@ export class CannerPATAuthenticator extends BaseAuthenticator operationName: 'UserMe', variables: {}, query: - 'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username}', + 'query UserMe{userMe {accountRole attributes createdAt email groups {id name} lastName firstName username}}', }, { headers: { @@ -113,11 +110,11 @@ export class CannerPATAuthenticator extends BaseAuthenticator } private getCannerUrl(path = '/') { const { host, port, ssl = false } = this.options; - if (process.env['IS_ON_KUBERNETES']) + if (config.isOnKubernetes) return `http://${process.env['WEB_SERVICE_HOST']}${path}`; // for internal usage, we don't need to specify port else { const protocol = ssl ? 'https' : 'http'; - return `${protocol}://${host}:${port}${path}`; + return `${protocol}://${host}${port ? `:${port}` : ''}${path}`; } } } diff --git a/packages/extension-authenticator-canner/src/lib/config.ts b/packages/extension-authenticator-canner/src/lib/config.ts new file mode 100644 index 00000000..9674dcc5 --- /dev/null +++ b/packages/extension-authenticator-canner/src/lib/config.ts @@ -0,0 +1,10 @@ +export interface IEnvConfig { + // indicates whether the extension is running in k8s + isOnKubernetes?: boolean; +} + +const config: IEnvConfig = { + isOnKubernetes: Boolean(process.env['IS_ON_KUBERNETES']) || false, +}; + +export default config; diff --git a/packages/extension-authenticator-canner/src/lib/index.ts b/packages/extension-authenticator-canner/src/lib/index.ts new file mode 100644 index 00000000..27ff2e44 --- /dev/null +++ b/packages/extension-authenticator-canner/src/lib/index.ts @@ -0,0 +1 @@ +export * from './authenticator/pat'; diff --git a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts b/packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts similarity index 97% rename from packages/serve/test/auth/cannerPATAuthenticator.spec.ts rename to packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts index 7edfb047..8d3d9188 100644 --- a/packages/serve/test/auth/cannerPATAuthenticator.spec.ts +++ b/packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts @@ -2,7 +2,7 @@ import * as sinon from 'ts-sinon'; import axios from 'axios'; import { IncomingHttpHeaders } from 'http'; import { Request } from 'koa'; -import { CannerPATAuthenticator } from '@vulcan-sql/serve/auth'; +import { CannerPATAuthenticator } from '../lib/authenticator'; import { AuthResult, AuthStatus, KoaContext } from '@vulcan-sql/serve/models'; jest.mock('axios'); @@ -44,7 +44,7 @@ const mockOptions = { }; it.each([ - { 'canner-pat': { host: 'mockHost' } }, + { 'canner-pat': { port: 3000, ssl: true } }, { 'canner-pat': { port: 3000 } }, { 'canner-pat': { ssl: false } }, { 'canner-pat': {} }, @@ -63,7 +63,7 @@ it.each([ // Act, Assert await expect(authenticator.authCredential(ctx)).rejects.toThrow( - 'please provide correct connection information to Canner Enterprise, including "host" and "port".' + 'please provide correct connection information to Canner Enterprise, including "host".' ); }); diff --git a/packages/extension-authenticator-canner/tsconfig.json b/packages/extension-authenticator-canner/tsconfig.json new file mode 100644 index 00000000..f5b85657 --- /dev/null +++ b/packages/extension-authenticator-canner/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/extension-authenticator-canner/tsconfig.lib.json b/packages/extension-authenticator-canner/tsconfig.lib.json new file mode 100644 index 00000000..1925baa1 --- /dev/null +++ b/packages/extension-authenticator-canner/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts", "../../types/*.d.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/extension-authenticator-canner/tsconfig.spec.json b/packages/extension-authenticator-canner/tsconfig.spec.json new file mode 100644 index 00000000..2c94a339 --- /dev/null +++ b/packages/extension-authenticator-canner/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "../../types/*.d.ts"] +} diff --git a/packages/integration-testing/src/examples/example2-auth-user.spec.ts b/packages/integration-testing/src/examples/example2-auth-user.spec.ts index a7aa9ae5..1b1bfdaf 100644 --- a/packages/integration-testing/src/examples/example2-auth-user.spec.ts +++ b/packages/integration-testing/src/examples/example2-auth-user.spec.ts @@ -3,7 +3,6 @@ import { ServeConfig, VulcanServer } from '@vulcan-sql/serve'; import * as supertest from 'supertest'; import * as md5 from 'md5'; import defaultConfig from './projectConfig'; -import faker from '@faker-js/faker'; let server: VulcanServer; @@ -76,39 +75,3 @@ it.each([...users])( }, 10000 ); - -it('Example 2: authenticate user identity by POST /auth/token API using PAT should get 400', async () => { - // Arrange - const projectConfig: ServeConfig & IBuildOptions = { - ...defaultConfig, - auth: { - enabled: true, - options: { - 'canner-pat': { - host: 'mockhost', - port: faker.datatype.number({ min: 20000, max: 30000 }), - ssl: false, - }, - }, - }, - }; - const builder = new VulcanBuilder(projectConfig); - await builder.build(); - server = new VulcanServer(projectConfig); - const httpServer = (await server.start())['http']; - // Act - const agent = supertest(httpServer); - - // Assert - const result = await agent - .post('/auth/token') - .send({ - type: 'canner-pat', - }) - .set('Accept', 'application/json') - .set('Authorization', 'Canner-PAT mocktoken'); - expect(result.status).toBe(400); - expect(JSON.parse(result.text).message).toBe( - 'canner-pat does not support token generate.' - ); -}, 10000); diff --git a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts b/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts deleted file mode 100644 index e4b8559f..00000000 --- a/packages/integration-testing/src/examples/example5-get-user-profile-CannerPAT.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { IBuildOptions, VulcanBuilder } from '@vulcan-sql/build'; -import { - ServeConfig, - VulcanServer, - CannerPATAuthenticator, -} from '@vulcan-sql/serve'; -import * as supertest from 'supertest'; -import defaultConfig from './projectConfig'; -import * as sinon from 'ts-sinon'; - -describe('Example3: get user profile by GET /auth/user-profile API with Authorization', () => { - let server: VulcanServer; - let projectConfig: ServeConfig & IBuildOptions; - const mockUser = { - username: 'apple Hey', - firstName: 'Hey', - lastName: 'apple', - accountRole: 'admin', - - attributes: { - attr1: 100 * 10000, - attr2: 'Los Angeles', - }, - createdAt: '2023-03-27T12:48:15.882Z', - email: 'Alvina_Farrell82@yahoo.com', - groups: [{ id: 1, name: 'group1' }], - }; - const mockToken = `Canner-PAT myPATToken`; - const mockCannerUserResponse = { - status: 200, - data: { - data: { - userMe: mockUser, - }, - }, - }; - const expectedUserProfile = { - name: mockUser.username, - attr: { - firstName: 'Hey', - lastName: 'apple', - accountRole: 'admin', - - attributes: { - attr1: 100 * 10000, - attr2: 'Los Angeles', - }, - createdAt: '2023-03-27T12:48:15.882Z', - email: 'Alvina_Farrell82@yahoo.com', - groups: [{ id: 1, name: 'group1' }], - }, - }; - // TODO: refactor to avoid stubbing private method - // stub the private function to manipulate getting user info from remote server - const stubFetchCannerUser = (user: any) => { - const stub = sinon.default.stub( - CannerPATAuthenticator.prototype, - 'fetchCannerUser' - ); - stub.resolves(user); - return stub; - }; - beforeEach(async () => { - projectConfig = { - ...defaultConfig, - auth: { - enabled: true, - options: { - 'canner-pat': { - host: 'mockhost', - port: 3000, - ssl: false, - }, - }, - }, - }; - }); - - afterEach(async () => { - sinon.default.restore(); - await server?.close(); - }); - - it('Example 3-1: set Authorization in header with default options', async () => { - stubFetchCannerUser(mockCannerUserResponse); - const builder = new VulcanBuilder(projectConfig); - await builder.build(); - server = new VulcanServer(projectConfig); - const httpServer = (await server.start())['http']; - - const agent = supertest(httpServer); - const result = await agent - .get('/auth/user-profile') - .set('Authorization', mockToken); - expect(result.body).toEqual(expectedUserProfile); - }, 10000); - - it('Example 3-2: set Authorization in querying with default options', async () => { - projectConfig['auth-source'] = { - options: { - key: 'x-auth', - }, - }; - stubFetchCannerUser(mockCannerUserResponse); - const builder = new VulcanBuilder(projectConfig); - await builder.build(); - server = new VulcanServer(projectConfig); - const httpServer = (await server.start())['http']; - - const agent = supertest(httpServer); - const auth = Buffer.from( - JSON.stringify({ - Authorization: `Canner-PAT ${Buffer.from(mockToken).toString( - 'base64' - )}`, - }) - ).toString('base64'); - const result = await agent.get(`/auth/user-profile?x-auth=${auth}`); - - expect(result.body).toEqual(expectedUserProfile); - }, 10000); - - it('Example 3-3: set Authorization in querying with specific auth "key" options', async () => { - projectConfig['auth-source'] = { - options: { - key: 'x-auth', - }, - }; - stubFetchCannerUser(mockCannerUserResponse); - const builder = new VulcanBuilder(projectConfig); - await builder.build(); - server = new VulcanServer(projectConfig); - const httpServer = (await server.start())['http']; - - const agent = supertest(httpServer); - const auth = Buffer.from( - JSON.stringify({ - Authorization: `Canner-PAT ${Buffer.from(mockToken).toString( - 'base64' - )}`, - }) - ).toString('base64'); - const result = await agent.get(`/auth/user-profile?x-auth=${auth}`); - - expect(result.body).toEqual(expectedUserProfile); - }, 10000); - - it('Example 3-4: set Authorization in json payload specific auth "x-key" options', async () => { - projectConfig['auth-source'] = { - options: { - key: 'x-auth', - in: 'payload', - }, - }; - stubFetchCannerUser(mockCannerUserResponse); - const builder = new VulcanBuilder(projectConfig); - await builder.build(); - server = new VulcanServer(projectConfig); - const httpServer = (await server.start())['http']; - - const auth = Buffer.from( - JSON.stringify({ - Authorization: `Canner-PAT ${Buffer.from(mockToken).toString( - 'base64' - )}`, - }) - ).toString('base64'); - - const agent = supertest(httpServer); - - const result = await agent - .get('/auth/user-profile') - .send({ - ['x-auth']: auth, - }) - .set('Accept', 'application/json'); - - expect(result.body).toEqual(expectedUserProfile); - }, 10000); -}); diff --git a/packages/serve/src/lib/auth/httpBasicAuthenticator.ts b/packages/serve/src/lib/auth/httpBasicAuthenticator.ts index 2417cdd9..18810880 100644 --- a/packages/serve/src/lib/auth/httpBasicAuthenticator.ts +++ b/packages/serve/src/lib/auth/httpBasicAuthenticator.ts @@ -115,6 +115,7 @@ export class BasicAuthenticator extends BaseAuthenticator { const authorize = context.request.headers['authorization']; if ( + !this.getOptions() || !authorize || !authorize.toLowerCase().startsWith(this.getExtensionId()!) ) diff --git a/packages/serve/src/lib/auth/index.ts b/packages/serve/src/lib/auth/index.ts index 47f4ed8f..988de126 100644 --- a/packages/serve/src/lib/auth/index.ts +++ b/packages/serve/src/lib/auth/index.ts @@ -1,16 +1,13 @@ export * from './simpleTokenAuthenticator'; export * from './passwordFileAuthenticator'; export * from './httpBasicAuthenticator'; -export * from './cannerPATAuthenticator'; import { SimpleTokenAuthenticator } from './simpleTokenAuthenticator'; import { PasswordFileAuthenticator } from './passwordFileAuthenticator'; import { BasicAuthenticator } from './httpBasicAuthenticator'; -import { CannerPATAuthenticator } from './cannerPATAuthenticator'; export const BuiltInAuthenticators = [ BasicAuthenticator, SimpleTokenAuthenticator, PasswordFileAuthenticator, - CannerPATAuthenticator, ]; diff --git a/packages/serve/src/lib/auth/passwordFileAuthenticator.ts b/packages/serve/src/lib/auth/passwordFileAuthenticator.ts index a019c5da..f4c85b29 100644 --- a/packages/serve/src/lib/auth/passwordFileAuthenticator.ts +++ b/packages/serve/src/lib/auth/passwordFileAuthenticator.ts @@ -96,6 +96,7 @@ export class PasswordFileAuthenticator extends BaseAuthenticator } public async initialize() { const names = Object.keys(this.authenticators); - if (this.enabled && isEmpty(this.options)) { - throw new ConfigurationError( - `please set at least one auth type and user credential when you enable the "auth" options, currently support types: "${names}".` - ); + if (this.enabled && !isEmpty(this.options)) { + // check setup auth type in options also valid in authenticators + Object.keys(this.options).map((type) => { + if (!names.includes(type)) + throw new ConfigurationError( + `The auth type "${type}" in options not supported, authenticator only supported ${names}.` + ); + }); } - // check setup auth type in options also valid in authenticators - Object.keys(this.options).map((type) => { - if (!names.includes(type)) - throw new ConfigurationError( - `The auth type "${type}" in options not supported, authenticator only supported ${names}.` - ); - }); for (const name of names) { const authenticator = this.authenticators[name]; diff --git a/packages/serve/src/models/extensions/authenticator.ts b/packages/serve/src/models/extensions/authenticator.ts index 6f501314..c42cf740 100644 --- a/packages/serve/src/models/extensions/authenticator.ts +++ b/packages/serve/src/models/extensions/authenticator.ts @@ -6,7 +6,6 @@ export enum AuthType { Basic = 'basic', PasswordFile = 'password-file', SimpleToken = 'simple-token', - CannerPAT = 'canner-pat', } export interface AuthUserInfo { diff --git a/tsconfig.base.json b/tsconfig.base.json index a83171ef..1c55f315 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -70,6 +70,9 @@ "@vulcan-sql/core/validators/built-in-validators/*": [ "packages/core/src/lib/validators/built-in-validators/*" ], + "@vulcan-sql/extension-authenticator-canner": [ + "packages/extension-authenticator-canner/src/index.ts" + ], "@vulcan-sql/extension-dbt": ["packages/extension-dbt/src/index"], "@vulcan-sql/extension-debug-tools": [ "packages/extension-debug-tools/src/index.ts" @@ -77,15 +80,15 @@ "@vulcan-sql/extension-driver-bq": [ "packages/extension-driver-bq/src/index.ts" ], + "@vulcan-sql/extension-driver-canner": [ + "packages/extension-driver-canner/src/index.ts" + ], "@vulcan-sql/extension-driver-duckdb": [ "packages/extension-driver-duckdb/src/index.ts" ], "@vulcan-sql/extension-driver-pg": [ "packages/extension-driver-pg/src/index.ts" ], - "@vulcan-sql/extension-driver-canner": [ - "packages/extension-driver-canner/src/index.ts" - ], "@vulcan-sql/extension-driver-snowflake": [ "packages/extension-driver-snowflake/src/index.ts" ], diff --git a/workspace.json b/workspace.json index 7e0f20a7..46b9579e 100644 --- a/workspace.json +++ b/workspace.json @@ -6,13 +6,14 @@ "cli": "packages/cli", "core": "packages/core", "doc": "packages/doc", + "extension-authenticator-canner": "packages/extension-authenticator-canner", "extension-dbt": "packages/extension-dbt", "extension-debug-tools": "packages/extension-debug-tools", "extension-driver-bq": "packages/extension-driver-bq", + "extension-driver-canner": "packages/extension-driver-canner", "extension-driver-duckdb": "packages/extension-driver-duckdb", "extension-driver-pg": "packages/extension-driver-pg", "extension-driver-snowflake": "packages/extension-driver-snowflake", - "extension-driver-canner": "packages/extension-driver-canner", "extension-store-canner": "packages/extension-store-canner", "integration-testing": "packages/integration-testing", "serve": "packages/serve", From 94a636ea6fadcd900161d285f5ea3bb837cb0a9a Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Mon, 12 Jun 2023 15:00:26 +0800 Subject: [PATCH 5/7] chore(feature/authenticator): add playground vulcan.yaml example --- labs/playground1/README.md | 4 ++ .../use-canner-pat-authenticator/vulcan.yaml | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml diff --git a/labs/playground1/README.md b/labs/playground1/README.md index 2bc79552..0417f295 100644 --- a/labs/playground1/README.md +++ b/labs/playground1/README.md @@ -19,3 +19,7 @@ make ## Testing Data After installation, you can find `artists.csv` and `artworks.csv` under folder `test-data`. They are the data we used for this playground. You can also access the data base via [DuckDB CLI](https://duckdb.org/docs/api/cli): `duckdb ./test-data/moma.db` + +## Examples + +We provide some examples in the `examples` folder to show how to configured your Vulcan API \ No newline at end of file diff --git a/labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml b/labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml new file mode 100644 index 00000000..143f4239 --- /dev/null +++ b/labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml @@ -0,0 +1,57 @@ +################################################################################################################### +# This is a vulcan.yaml example of using extension extension-authenticator-canner to authenticate your API request with Canner PAT +################################################################################################################### +name: playground1 +description: A starter Vulcan project +version: 0.1.0-alpha.1 +template: + provider: LocalFile + # Path to .sql files + folderPath: sqls + codeLoader: InMemory +artifact: + provider: LocalFile + serializer: JSON + # Path to build result + filePath: result.json +schema-parser: + reader: LocalFile + # Path to .yaml files + folderPath: sqls +document-generator: + specs: + - oas3 +types: + - RESTFUL +extensions: + duckdb: '@vulcan-sql/extension-driver-duckdb' + # name my extension as canner-pat + canner-pat: '@vulcan-sql/extension-authenticator-canner' + +profiles: + - profile.yaml +rate-limit: + options: + interval: + min: 1 + max: 10000 +enforce-https: + enabled: false +auth: + enabled: true + +response-format: + enabled: true + options: + default: json + formats: + - json + - csv +# here is my definition of canner-pat +canner-pat: + options: + canner-pat: + host: your-canner-host + port: 443 + # use https protocol to connect to canner server, change it to false if you are using http + ssl: true \ No newline at end of file From 3d3ab854be15cb35a9c56345f26b60aa9d90f62d Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Mon, 12 Jun 2023 16:36:43 +0800 Subject: [PATCH 6/7] fix(feat/authenticator): remove test cases cause logic change --- .../authCredentialMiddleware.spec.ts | 21 ------------------- .../authRouteMiddleware.spec.ts | 17 --------------- 2 files changed, 38 deletions(-) diff --git a/packages/serve/test/middlewares/built-in-middlewares/authCredentialMiddleware.spec.ts b/packages/serve/test/middlewares/built-in-middlewares/authCredentialMiddleware.spec.ts index 52994804..826270e5 100644 --- a/packages/serve/test/middlewares/built-in-middlewares/authCredentialMiddleware.spec.ts +++ b/packages/serve/test/middlewares/built-in-middlewares/authCredentialMiddleware.spec.ts @@ -38,27 +38,6 @@ describe('Test auth credential middleware', () => { expect(spy).not.toHaveBeenCalled(); }); - it.each([[{}], [undefined]])( - 'Should throw error when options = %p', - async (options) => { - // Arrange - const expected = new Error( - 'please set at least one auth type and user credential when you enable the "auth" options, currently support types: "".' - ); - - // Act - const middleware = new AuthCredentialsMiddleware( - { options: options }, - '', - [], - new ProjectOptions() - ); - const activateFunc = async () => await middleware.activate(); - - expect(activateFunc).rejects.toThrow(expected); - } - ); - it.each([ ['basic', sinon.stubInterface()], ['simple-token', sinon.stubInterface()], diff --git a/packages/serve/test/middlewares/built-in-middlewares/authRouteMiddleware.spec.ts b/packages/serve/test/middlewares/built-in-middlewares/authRouteMiddleware.spec.ts index af807bef..02dfc05a 100644 --- a/packages/serve/test/middlewares/built-in-middlewares/authRouteMiddleware.spec.ts +++ b/packages/serve/test/middlewares/built-in-middlewares/authRouteMiddleware.spec.ts @@ -60,23 +60,6 @@ describe('Test auth router middleware', () => { sinon.default.restore(); }); - it.each([[{}], [undefined]])( - 'Should throw error when options = %p', - async (options) => { - // Arrange - const expected = new Error( - 'please set at least one auth type and user credential when you enable the "auth" options, currently support types: "".' - ); - - // Act - const middleware = new AuthRouterMiddleware({ options: options }, '', []); - - const activateFunc = async () => await middleware.activate(); - - expect(activateFunc).rejects.toThrow(expected); - } - ); - it('Should active corresponding authenticator when activate middleware', async () => { // Arrange const middleware = new AuthRouterMiddleware( From 646b3d810a7f3ee35274aad2001e36b847d88560 Mon Sep 17 00:00:00 2001 From: onlyjackfrost Date: Mon, 12 Jun 2023 18:18:39 +0800 Subject: [PATCH 7/7] chore(feature/authenticator): fix typo --- .../use-canner-pat-authenticator/vulcan.yaml | 57 ------------------- .../extension-authenticator-canner/README.md | 10 +--- .../src/test/cannerPATAuthenticator.spec.ts | 2 +- .../test/cannerAdapter.spec.ts | 2 +- .../test/cannerDataSource.spec.ts | 6 +- 5 files changed, 6 insertions(+), 71 deletions(-) delete mode 100644 labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml diff --git a/labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml b/labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml deleted file mode 100644 index 143f4239..00000000 --- a/labs/playground1/examples/use-canner-pat-authenticator/vulcan.yaml +++ /dev/null @@ -1,57 +0,0 @@ -################################################################################################################### -# This is a vulcan.yaml example of using extension extension-authenticator-canner to authenticate your API request with Canner PAT -################################################################################################################### -name: playground1 -description: A starter Vulcan project -version: 0.1.0-alpha.1 -template: - provider: LocalFile - # Path to .sql files - folderPath: sqls - codeLoader: InMemory -artifact: - provider: LocalFile - serializer: JSON - # Path to build result - filePath: result.json -schema-parser: - reader: LocalFile - # Path to .yaml files - folderPath: sqls -document-generator: - specs: - - oas3 -types: - - RESTFUL -extensions: - duckdb: '@vulcan-sql/extension-driver-duckdb' - # name my extension as canner-pat - canner-pat: '@vulcan-sql/extension-authenticator-canner' - -profiles: - - profile.yaml -rate-limit: - options: - interval: - min: 1 - max: 10000 -enforce-https: - enabled: false -auth: - enabled: true - -response-format: - enabled: true - options: - default: json - formats: - - json - - csv -# here is my definition of canner-pat -canner-pat: - options: - canner-pat: - host: your-canner-host - port: 443 - # use https protocol to connect to canner server, change it to false if you are using http - ssl: true \ No newline at end of file diff --git a/packages/extension-authenticator-canner/README.md b/packages/extension-authenticator-canner/README.md index 4afe3116..9919d62e 100644 --- a/packages/extension-authenticator-canner/README.md +++ b/packages/extension-authenticator-canner/README.md @@ -35,12 +35,4 @@ This extension let Data API request can be authenticated with [Canner PAT](https post: 443 # indicate using http or https default is false ssl: true - ``` - -## Testing - -```bash -nx test extension-authenticator-canner -``` - -This library was generated with [Nx](https://nx.dev). + ``` \ No newline at end of file diff --git a/packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts b/packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts index 8d3d9188..5d87b28b 100644 --- a/packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts +++ b/packages/extension-authenticator-canner/src/test/cannerPATAuthenticator.spec.ts @@ -199,7 +199,7 @@ it('Should auth credential successful when request header "authorization" pass t }); // Token info -it('Should throw when use getTokenInfo with cannerPATAuthenticator', async () => { +it('Should throw error when use getTokenInfo with cannerPATAuthenticator', async () => { // Arrange const ctx = { ...sinon.stubInterface(), diff --git a/packages/extension-driver-canner/test/cannerAdapter.spec.ts b/packages/extension-driver-canner/test/cannerAdapter.spec.ts index 2fb8b444..37503c84 100644 --- a/packages/extension-driver-canner/test/cannerAdapter.spec.ts +++ b/packages/extension-driver-canner/test/cannerAdapter.spec.ts @@ -12,7 +12,7 @@ it('CannerAdapter should get urls without throw any error when connection and sq adapter.createAsyncQueryResultUrls('select 1') ).resolves.not.toThrow(); }, 50000); -it('CannerAdapter should throw when connection or sql are invalid', async () => { +it('CannerAdapter should throw error when connection or sql are invalid', async () => { // Arrange const { connection } = pg.getProfile('profile1'); const adapter = new CannerAdapter(connection); diff --git a/packages/extension-driver-canner/test/cannerDataSource.spec.ts b/packages/extension-driver-canner/test/cannerDataSource.spec.ts index 18d30ba6..9daf0af7 100644 --- a/packages/extension-driver-canner/test/cannerDataSource.spec.ts +++ b/packages/extension-driver-canner/test/cannerDataSource.spec.ts @@ -60,7 +60,7 @@ it('Data source should export successfully', async () => { fs.rmSync('tmp', { recursive: true, force: true }); }, 100000); -it('Data source should throw when fail to export data', async () => { +it('Data source should throw error when fail to export data', async () => { // Arrange // TODO: refactor to avoid stubbing private method // stub the private function to manipulate getting error from the remote server @@ -91,7 +91,7 @@ it('Data source should throw when fail to export data', async () => { fs.rmSync('tmp', { recursive: true, force: true }); }, 100000); -it('Data source should throw when given directory is not exist', async () => { +it('Data source should throw error when given directory is not exist', async () => { // Arrange dataSource = new CannerDataSource({}, '', [pg.getProfile('profile1')]); await dataSource.activate(); @@ -106,7 +106,7 @@ it('Data source should throw when given directory is not exist', async () => { ).rejects.toThrow(); }, 100000); -it('Data source should throw when given profile name is not exist', async () => { +it('Data source should throw error when given profile name is not exist', async () => { // Arrange dataSource = new CannerDataSource({}, '', [pg.getProfile('profile1')]); await dataSource.activate();