Skip to content

Commit 87882e8

Browse files
committed
chore(native): Native gateway - expose API from node
1 parent bf09dae commit 87882e8

File tree

25 files changed

+766
-168
lines changed

25 files changed

+766
-168
lines changed

packages/cubejs-api-gateway/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"typings": "dist/src/index.d.ts",
1616
"scripts": {
1717
"test": "npm run unit",
18-
"unit": "jest --coverage dist/test",
18+
"unit": "CUBE_JS_NATIVE_API_GATEWAY_INTERNAL=true jest --coverage dist/test",
1919
"build": "rm -rf dist && npm run tsc",
2020
"tsc": "tsc",
2121
"watch": "tsc -w",

packages/cubejs-api-gateway/src/gateway.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,10 @@ function systemAsyncHandler(handler: (req: Request & { context: ExtendedRequestC
120120
};
121121
}
122122

123-
// Prepared CheckAuthFn, default or from config: always async, returns nothing
124-
type PreparedCheckAuthFn = (ctx: any, authorization?: string) => Promise<void>;
123+
// Prepared CheckAuthFn, default or from config: always async
124+
type PreparedCheckAuthFn = (ctx: any, authorization?: string) => Promise<{
125+
securityContext: any;
126+
}>;
125127

126128
class ApiGateway {
127129
protected readonly refreshScheduler: any;
@@ -148,9 +150,9 @@ class ApiGateway {
148150

149151
public readonly checkAuthSystemFn: PreparedCheckAuthFn;
150152

151-
protected readonly contextToApiScopesFn: ContextToApiScopesFn;
153+
public readonly contextToApiScopesFn: ContextToApiScopesFn;
152154

153-
protected readonly contextToApiScopesDefFn: ContextToApiScopesFn =
155+
public readonly contextToApiScopesDefFn: ContextToApiScopesFn =
154156
async () => ['graphql', 'meta', 'data'];
155157

156158
protected readonly requestLoggerMiddleware: RequestLoggerMiddlewareFn;
@@ -544,20 +546,24 @@ class ApiGateway {
544546
}
545547

546548
if (getEnv('nativeApiGateway')) {
547-
const proxyMiddleware = createProxyMiddleware<Request, Response>({
548-
target: `http://127.0.0.1:${this.sqlServer.getNativeGatewayPort()}/v2`,
549-
changeOrigin: true,
550-
});
551-
552-
app.use(
553-
`${this.basePath}/v2`,
554-
proxyMiddleware as any
555-
);
549+
this.enableNativeApiGateway(app);
556550
}
557551

558552
app.use(this.handleErrorMiddleware);
559553
}
560554

555+
protected enableNativeApiGateway(app: ExpressApplication) {
556+
const proxyMiddleware = createProxyMiddleware<Request, Response>({
557+
target: `http://127.0.0.1:${this.sqlServer.getNativeGatewayPort()}/v2`,
558+
changeOrigin: true,
559+
});
560+
561+
app.use(
562+
`${this.basePath}/v2`,
563+
proxyMiddleware as any
564+
);
565+
}
566+
561567
public initSubscriptionServer(sendMessage: WebSocketSendMessageFn) {
562568
return new SubscriptionServer(this, sendMessage, this.subscriptionStore, this.wsContextAcceptor);
563569
}
@@ -2250,6 +2256,10 @@ class ApiGateway {
22502256

22512257
showWarningAboutNotObject = true;
22522258
}
2259+
2260+
return {
2261+
securityContext: req.securityContext
2262+
};
22532263
};
22542264
}
22552265

@@ -2333,6 +2343,10 @@ class ApiGateway {
23332343
// @todo Move it to 401 or 400
23342344
throw new CubejsHandlerError(403, 'Forbidden', 'Authorization header isn\'t set');
23352345
}
2346+
2347+
return {
2348+
securityContext: req.securityContext
2349+
};
23362350
};
23372351
}
23382352

@@ -2343,6 +2357,7 @@ class ApiGateway {
23432357

23442358
if (this.playgroundAuthSecret) {
23452359
const systemCheckAuthFn = this.createCheckAuthSystemFn();
2360+
23462361
return async (ctx, authorization) => {
23472362
// TODO: separate two auth workflows
23482363
try {
@@ -2354,6 +2369,10 @@ class ApiGateway {
23542369
throw mainAuthError;
23552370
}
23562371
}
2372+
2373+
return {
2374+
securityContext: ctx.securityContext,
2375+
};
23572376
};
23582377
}
23592378

@@ -2371,6 +2390,10 @@ class ApiGateway {
23712390

23722391
return async (ctx, authorization) => {
23732392
await systemCheckAuthFn(ctx, authorization);
2393+
2394+
return {
2395+
securityContext: ctx.securityContext
2396+
};
23742397
};
23752398
}
23762399

packages/cubejs-api-gateway/src/sql-server.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
setupLogger,
3+
resetLogger,
34
registerInterface,
45
shutdownInterface,
56
execSql,
@@ -14,7 +15,7 @@ import { displayCLIWarning, getEnv } from '@cubejs-backend/shared';
1415

1516
import * as crypto from 'crypto';
1617
import type { ApiGateway } from './gateway';
17-
import type { CheckSQLAuthFn, ExtendedRequestContext, CanSwitchSQLUserFn } from './interfaces';
18+
import type { CheckAuthFn, CheckSQLAuthFn, ExtendedRequestContext, CanSwitchSQLUserFn } from './interfaces';
1819

1920
export type SQLServerOptions = {
2021
checkSqlAuth?: CheckSQLAuthFn,
@@ -107,6 +108,19 @@ export class SQLServer {
107108
this.sqlInterfaceInstance = await registerInterface({
108109
gatewayPort: this.gatewayPort,
109110
pgPort: options.pgSqlPort,
111+
contextToApiScopes: async ({ securityContext }) => {
112+
return this.apiGateway.contextToApiScopesFn(
113+
securityContext,
114+
getEnv('defaultApiScope') || await this.apiGateway.contextToApiScopesDefFn()
115+
);
116+
},
117+
checkAuth: async ({ request, token }) => {
118+
const { securityContext } = await this.apiGateway.checkAuthFn(request, token);
119+
120+
return {
121+
securityContext
122+
};
123+
},
110124
checkSqlAuth: async ({ request, user, password }) => {
111125
const { password: returnedPassword, superuser, securityContext, skipPasswordCheck } = await checkSqlAuth(request, user, password);
112126

@@ -369,5 +383,9 @@ export class SQLServer {
369383

370384
public async shutdown(mode: ShutdownMode): Promise<void> {
371385
await shutdownInterface(this.sqlInterfaceInstance!, mode);
386+
387+
resetLogger(
388+
process.env.CUBEJS_LOG_LEVEL === 'trace' ? 'trace' : 'warn'
389+
);
372390
}
373391
}

packages/cubejs-api-gateway/test/auth.test.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,54 @@ import express, { Application as ExpressApplication, RequestHandler } from 'expr
33
// eslint-disable-next-line import/no-extraneous-dependencies
44
import request from 'supertest';
55
import jwt from 'jsonwebtoken';
6-
import { pausePromise } from '@cubejs-backend/shared';
6+
import {getEnv, pausePromise} from '@cubejs-backend/shared';
77

88
import { ApiGateway, ApiGatewayOptions, CubejsHandlerError, Request } from '../src';
99
import { AdapterApiMock, DataSourceStorageMock } from './mocks';
1010
import { RequestContext } from '../src/interfaces';
1111
import { generateAuthToken } from './utils';
1212

13+
class ApiGatewayOpenAPI extends ApiGateway {
14+
protected isRunning: Promise<void> | null = null;
15+
16+
public coerceForSqlQuery(query, context: RequestContext) {
17+
return super.coerceForSqlQuery(query, context);
18+
}
19+
20+
public async startSQLServer(): Promise<void> {
21+
if (this.isRunning) {
22+
return this.isRunning;
23+
}
24+
25+
this.isRunning = this.sqlServer.init({});
26+
27+
return this.isRunning;
28+
}
29+
30+
public async shutdownSQLServer(): Promise<void> {
31+
try {
32+
await this.sqlServer.shutdown('fast');
33+
} catch (error) {
34+
console.log(`Error while shutting down server: ${error}`);
35+
}
36+
37+
this.isRunning = null;
38+
}
39+
}
40+
1341
function createApiGateway(handler: RequestHandler, logger: () => any, options: Partial<ApiGatewayOptions>) {
1442
const adapterApi: any = new AdapterApiMock();
1543
const dataSourceStorage: any = new DataSourceStorageMock();
1644

17-
class ApiGatewayFake extends ApiGateway {
18-
public coerceForSqlQuery(query, context: RequestContext) {
19-
return super.coerceForSqlQuery(query, context);
20-
}
21-
45+
class ApiGatewayFake extends ApiGatewayOpenAPI {
2246
public initApp(app: ExpressApplication) {
2347
const userMiddlewares: RequestHandler[] = [
2448
this.checkAuth,
2549
this.requestContextMiddleware,
2650
];
2751

2852
app.get('/test-auth-fake', userMiddlewares, handler);
53+
this.enableNativeApiGateway(app);
2954

3055
app.use(this.handleErrorMiddleware);
3156
}
@@ -50,6 +75,61 @@ function createApiGateway(handler: RequestHandler, logger: () => any, options: P
5075
};
5176
}
5277

78+
describe('test authorization with native gateway', () => {
79+
const expectSecurityContext = (securityContext) => {
80+
expect(securityContext.uid).toEqual(5);
81+
expect(securityContext.iat).toBeDefined();
82+
expect(securityContext.exp).toBeDefined();
83+
};
84+
85+
let app: ExpressApplication;
86+
let apiGateway: ApiGatewayOpenAPI;
87+
88+
const handlerMock = jest.fn((req, res) => {
89+
expectSecurityContext(req.context.authInfo);
90+
expectSecurityContext(req.context.securityContext);
91+
92+
res.status(200).end();
93+
});
94+
const loggerMock = jest.fn(() => {
95+
//
96+
});
97+
98+
beforeAll(async () => {
99+
const result = createApiGateway(handlerMock, loggerMock, {});
100+
101+
app = result.app;
102+
apiGateway = result.apiGateway;
103+
104+
await result.apiGateway.startSQLServer();
105+
});
106+
107+
beforeEach(() => {
108+
handlerMock.mockClear();
109+
loggerMock.mockClear();
110+
});
111+
112+
afterAll(async () => {
113+
await apiGateway.shutdownSQLServer();
114+
});
115+
116+
it('default authorization', async () => {
117+
const token = generateAuthToken({ uid: 5, });
118+
119+
await request(app)
120+
.get('/cubejs-api/v2/stream')
121+
.set('Authorization', `${token}`)
122+
.expect(501);
123+
124+
// No bad logs
125+
expect(loggerMock.mock.calls.length).toEqual(0);
126+
// We should not call js handler, request should go into rust code
127+
expect(handlerMock.mock.calls.length).toEqual(0);
128+
129+
await apiGateway.shutdownSQLServer();
130+
});
131+
});
132+
53133
describe('test authorization', () => {
54134
test('default authorization', async () => {
55135
const loggerMock = jest.fn(() => {

0 commit comments

Comments
 (0)