Skip to content

Commit 33211b1

Browse files
authored
chore(native): Native gateway - support check_auth, context_to_api_scopes (#9377)
1 parent ab54e28 commit 33211b1

File tree

27 files changed

+879
-181
lines changed

27 files changed

+879
-181
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 --forceExit dist/test",
1919
"build": "rm -rf dist && npm run tsc",
2020
"tsc": "tsc",
2121
"watch": "tsc -w",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class SubscriptionServer {
5353
}
5454

5555
if (message.authorization) {
56-
authContext = { isSubscription: true };
56+
authContext = { isSubscription: true, protocol: 'ws' };
5757
await this.apiGateway.checkAuthFn(authContext, message.authorization);
5858
const acceptanceResult = await this.contextAcceptor(authContext);
5959
if (!acceptanceResult.accepted) {

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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ export class SQLServer {
107107
this.sqlInterfaceInstance = await registerInterface({
108108
gatewayPort: this.gatewayPort,
109109
pgPort: options.pgSqlPort,
110+
contextToApiScopes: async ({ securityContext }) => this.apiGateway.contextToApiScopesFn(
111+
securityContext,
112+
getEnv('defaultApiScope') || await this.apiGateway.contextToApiScopesDefFn()
113+
),
114+
checkAuth: async ({ request, token }) => {
115+
const { securityContext } = await this.apiGateway.checkAuthFn(request, token);
116+
117+
return {
118+
securityContext
119+
};
120+
},
110121
checkSqlAuth: async ({ request, user, password }) => {
111122
const { password: returnedPassword, superuser, securityContext, skipPasswordCheck } = await checkSqlAuth(request, user, password);
112123

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

Lines changed: 150 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,57 @@ import express, { Application as ExpressApplication, RequestHandler } from 'expr
44
import request from 'supertest';
55
import jwt from 'jsonwebtoken';
66
import { pausePromise } from '@cubejs-backend/shared';
7+
import { resetLogger } from '@cubejs-backend/native';
78

8-
import { ApiGateway, ApiGatewayOptions, CubejsHandlerError, Request } from '../src';
9+
import { ApiGateway, ApiGatewayOptions, CubejsHandlerError, Request, RequestContext } from '../src';
910
import { AdapterApiMock, DataSourceStorageMock } from './mocks';
10-
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+
} finally {
34+
this.isRunning = null;
35+
}
36+
37+
// SQLServer changes logger for rust side with setupLogger in the constructor, but it leads
38+
// to a memory leak, that's why jest doesn't allow to shut down tests
39+
resetLogger(
40+
process.env.CUBEJS_LOG_LEVEL === 'trace' ? 'trace' : 'warn'
41+
);
42+
}
43+
}
44+
1345
function createApiGateway(handler: RequestHandler, logger: () => any, options: Partial<ApiGatewayOptions>) {
1446
const adapterApi: any = new AdapterApiMock();
1547
const dataSourceStorage: any = new DataSourceStorageMock();
1648

17-
class ApiGatewayFake extends ApiGateway {
18-
public coerceForSqlQuery(query, context: RequestContext) {
19-
return super.coerceForSqlQuery(query, context);
20-
}
21-
49+
class ApiGatewayFake extends ApiGatewayOpenAPI {
2250
public initApp(app: ExpressApplication) {
2351
const userMiddlewares: RequestHandler[] = [
2452
this.checkAuth,
2553
this.requestContextMiddleware,
2654
];
2755

2856
app.get('/test-auth-fake', userMiddlewares, handler);
57+
this.enableNativeApiGateway(app);
2958

3059
app.use(this.handleErrorMiddleware);
3160
}
@@ -41,6 +70,7 @@ function createApiGateway(handler: RequestHandler, logger: () => any, options: P
4170
});
4271

4372
process.env.NODE_ENV = 'unknown';
73+
4474
const app = express();
4575
apiGateway.initApp(app);
4676

@@ -50,6 +80,119 @@ function createApiGateway(handler: RequestHandler, logger: () => any, options: P
5080
};
5181
}
5282

83+
describe('test authorization with native gateway', () => {
84+
let app: ExpressApplication;
85+
let apiGateway: ApiGatewayOpenAPI;
86+
87+
const handlerMock = jest.fn(() => {
88+
// nothing, we are using it to verify that we don't got to express code
89+
});
90+
const loggerMock = jest.fn(() => {
91+
//
92+
});
93+
const checkAuthMock = jest.fn((req, token) => {
94+
jwt.verify(token, 'secret');
95+
96+
return {
97+
security_context: {}
98+
};
99+
});
100+
101+
beforeAll(async () => {
102+
const result = createApiGateway(handlerMock, loggerMock, {
103+
checkAuth: checkAuthMock,
104+
gatewayPort: 8585,
105+
});
106+
107+
app = result.app;
108+
apiGateway = result.apiGateway;
109+
110+
await result.apiGateway.startSQLServer();
111+
});
112+
113+
beforeEach(() => {
114+
handlerMock.mockClear();
115+
loggerMock.mockClear();
116+
checkAuthMock.mockClear();
117+
});
118+
119+
afterAll(async () => {
120+
await apiGateway.shutdownSQLServer();
121+
});
122+
123+
it('default authorization - success', async () => {
124+
const token = generateAuthToken({ uid: 5, });
125+
126+
await request(app)
127+
.get('/cubejs-api/v2/stream')
128+
.set('Authorization', `${token}`)
129+
.send()
130+
.expect(501);
131+
132+
// No bad logs
133+
expect(loggerMock.mock.calls.length).toEqual(0);
134+
// We should not call js handler, request should go into rust code
135+
expect(handlerMock.mock.calls.length).toEqual(0);
136+
137+
// Verify that we passed token to JS side
138+
expect(checkAuthMock.mock.calls.length).toEqual(1);
139+
expect(checkAuthMock.mock.calls[0][0].protocol).toEqual('http');
140+
expect(checkAuthMock.mock.calls[0][1]).toEqual(token);
141+
});
142+
143+
it('default authorization - success (bearer prefix)', async () => {
144+
const token = generateAuthToken({ uid: 5, });
145+
146+
await request(app)
147+
.get('/cubejs-api/v2/stream')
148+
.set('Authorization', `Bearer ${token}`)
149+
.send()
150+
.expect(501);
151+
152+
// No bad logs
153+
expect(loggerMock.mock.calls.length).toEqual(0);
154+
// We should not call js handler, request should go into rust code
155+
expect(handlerMock.mock.calls.length).toEqual(0);
156+
157+
// Verify that we passed token to JS side
158+
expect(checkAuthMock.mock.calls.length).toEqual(1);
159+
expect(checkAuthMock.mock.calls[0][0].protocol).toEqual('http');
160+
expect(checkAuthMock.mock.calls[0][1]).toEqual(token);
161+
});
162+
163+
it('default authorization - wrong secret', async () => {
164+
const badToken = 'SUPER_LARGE_BAD_TOKEN_WHICH_IS_NOT_A_TOKEN';
165+
166+
await request(app)
167+
.get('/cubejs-api/v2/stream')
168+
.set('Authorization', `${badToken}`)
169+
.send()
170+
.expect(401);
171+
172+
// No bad logs
173+
expect(loggerMock.mock.calls.length).toEqual(0);
174+
// We should not call js handler, request should go into rust code
175+
expect(handlerMock.mock.calls.length).toEqual(0);
176+
177+
// Verify that we passed token to JS side
178+
expect(checkAuthMock.mock.calls.length).toEqual(1);
179+
expect(checkAuthMock.mock.calls[0][0].protocol).toEqual('http');
180+
expect(checkAuthMock.mock.calls[0][1]).toEqual(badToken);
181+
});
182+
183+
it('default authorization - missing auth header', async () => {
184+
await request(app)
185+
.get('/cubejs-api/v2/stream')
186+
.send()
187+
.expect(401);
188+
189+
// No bad logs
190+
expect(loggerMock.mock.calls.length).toEqual(0);
191+
// We should not call js handler, request should go into rust code
192+
expect(handlerMock.mock.calls.length).toEqual(0);
193+
});
194+
});
195+
53196
describe('test authorization', () => {
54197
test('default authorization', async () => {
55198
const loggerMock = jest.fn(() => {

0 commit comments

Comments
 (0)