Skip to content

Commit aad6aa3

Browse files
authored
feat(server-core, api-gateway): Permissions API (#6240)
1 parent 327350d commit aad6aa3

File tree

11 files changed

+557
-64
lines changed

11 files changed

+557
-64
lines changed

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

Lines changed: 203 additions & 64 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
CheckSQLAuthSuccessResponse,
2929
CheckSQLAuthFn,
3030
CanSwitchSQLUserFn,
31+
ContextToPermissionsFn,
3132
} from './types/auth';
3233

3334
import {
@@ -66,6 +67,7 @@ export {
6667
ExtendContextFn,
6768
ResponseResultFn,
6869
QueryRequest,
70+
ContextToPermissionsFn,
6971
};
7072

7173
/**

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
* Cube.js auth related data types definition.
66
*/
77

8+
import { Permission } from './strings';
9+
10+
/**
11+
* Permissions tuple.
12+
*/
13+
type PermissionsTuple = Permission[];
14+
815
/**
916
* Internal auth logic options object data type.
1017
*/
@@ -70,11 +77,19 @@ type CanSwitchSQLUserFn =
7077
Promise<boolean> |
7178
boolean;
7279

80+
/**
81+
* Returns permissions tuple from a security context.
82+
*/
83+
type ContextToPermissionsFn =
84+
(securityContext?: any) => Promise<PermissionsTuple>;
85+
7386
export {
7487
CheckAuthInternalOptions,
7588
JWTOptions,
7689
CheckAuthFn,
7790
CheckSQLAuthSuccessResponse,
7891
CheckSQLAuthFn,
7992
CanSwitchSQLUserFn,
93+
PermissionsTuple,
94+
ContextToPermissionsFn,
8095
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
RequestLoggerMiddlewareFn,
1919
ContextRejectionMiddlewareFn,
2020
ContextAcceptorFn,
21+
ContextToPermissionsFn,
2122
} from '../interfaces';
2223

2324
type UserBackgroundContext = {
@@ -68,6 +69,7 @@ interface ApiGatewayOptions {
6869
* @deprecated Use checkAuth property instead.
6970
*/
7071
checkAuthMiddleware?: CheckAuthMiddlewareFn;
72+
contextToPermissions?: ContextToPermissionsFn;
7173
}
7274

7375
export {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ type QueryOrderType =
9898
'asc' |
9999
'desc';
100100

101+
/**
102+
* Permission data type.
103+
*/
104+
type Permission =
105+
'liveliness' |
106+
'graphql' |
107+
'meta' |
108+
'data' |
109+
'jobs';
110+
101111
export {
102112
RequestType,
103113
ResultType,
@@ -109,4 +119,5 @@ export {
109119
FilterOperator,
110120
QueryTimeDimensionGranularity,
111121
QueryOrderType,
122+
Permission,
112123
};
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import express from 'express';
2+
import request from 'supertest';
3+
import { ApiGateway, ApiGatewayOptions, Query, Request } from '../src';
4+
import {
5+
compilerApi,
6+
DataSourceStorageMock,
7+
AdapterApiMock
8+
} from './mocks';
9+
10+
const API_SECRET = 'secret';
11+
const AUTH_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M';
12+
const logger = () => undefined;
13+
function createApiGateway(
14+
options: Partial<ApiGatewayOptions> = {}
15+
) {
16+
process.env.NODE_ENV = 'production';
17+
18+
const app = express();
19+
const adapterApi: any = new AdapterApiMock();
20+
const dataSourceStorage: any = new DataSourceStorageMock();
21+
const apiGateway = new ApiGateway(API_SECRET, compilerApi, () => adapterApi, logger, {
22+
standalone: true,
23+
dataSourceStorage,
24+
basePath: '/cubejs-api',
25+
refreshScheduler: {},
26+
...options,
27+
});
28+
apiGateway.initApp(app);
29+
return {
30+
app,
31+
apiGateway,
32+
dataSourceStorage,
33+
adapterApi
34+
};
35+
}
36+
37+
describe('Gateway permissions', () => {
38+
test('Liveliness declined', async () => {
39+
const { app, apiGateway } = createApiGateway({
40+
contextToPermissions: async () => new Promise((resolve) => {
41+
resolve(['graphql', 'meta', 'data', 'jobs']);
42+
}),
43+
});
44+
45+
await request(app)
46+
.get('/readyz')
47+
.set('Authorization', AUTH_TOKEN)
48+
.expect(200);
49+
50+
await request(app)
51+
.get('/livez')
52+
.set('Authorization', AUTH_TOKEN)
53+
.expect(200);
54+
55+
apiGateway.release();
56+
});
57+
58+
test('GraphQL declined', async () => {
59+
const { app, apiGateway } = createApiGateway({
60+
contextToPermissions: async () => new Promise((resolve) => {
61+
resolve(['liveliness', 'meta', 'data', 'jobs']);
62+
}),
63+
});
64+
65+
const res = await request(app)
66+
.get('/cubejs-api/graphql')
67+
.set('Authorization', AUTH_TOKEN)
68+
.expect(403);
69+
70+
expect(res.body && res.body.error)
71+
.toStrictEqual('Permission is not allowed: graphql');
72+
73+
apiGateway.release();
74+
});
75+
76+
test('Meta declined', async () => {
77+
const { app, apiGateway } = createApiGateway({
78+
contextToPermissions: async () => new Promise((resolve) => {
79+
resolve(['liveliness', 'graphql', 'data', 'jobs']);
80+
}),
81+
});
82+
83+
const res1 = await request(app)
84+
.get('/cubejs-api/v1/meta')
85+
.set('Authorization', AUTH_TOKEN)
86+
.expect(403);
87+
88+
expect(res1.body && res1.body.error)
89+
.toStrictEqual('Permission is not allowed: meta');
90+
91+
const res2 = await request(app)
92+
.post('/cubejs-api/v1/pre-aggregations/can-use')
93+
.set('Authorization', AUTH_TOKEN)
94+
.expect(403);
95+
96+
expect(res2.body && res2.body.error)
97+
.toStrictEqual('Permission is not allowed: meta');
98+
99+
apiGateway.release();
100+
});
101+
102+
test('Data declined', async () => {
103+
const { app, apiGateway } = createApiGateway({
104+
contextToPermissions: async () => new Promise((resolve) => {
105+
resolve(['liveliness', 'graphql', 'meta', 'jobs']);
106+
}),
107+
});
108+
109+
const res1 = await request(app)
110+
.get('/cubejs-api/v1/load')
111+
.set('Authorization', AUTH_TOKEN)
112+
.expect(403);
113+
114+
expect(res1.body && res1.body.error)
115+
.toStrictEqual('Permission is not allowed: data');
116+
117+
const res2 = await request(app)
118+
.post('/cubejs-api/v1/load')
119+
.set('Authorization', AUTH_TOKEN)
120+
.expect(403);
121+
122+
expect(res2.body && res2.body.error)
123+
.toStrictEqual('Permission is not allowed: data');
124+
125+
const res3 = await request(app)
126+
.get('/cubejs-api/v1/subscribe')
127+
.set('Authorization', AUTH_TOKEN)
128+
.expect(403);
129+
130+
expect(res3.body && res3.body.error)
131+
.toStrictEqual('Permission is not allowed: data');
132+
133+
const res4 = await request(app)
134+
.get('/cubejs-api/v1/sql')
135+
.set('Authorization', AUTH_TOKEN)
136+
.expect(403);
137+
138+
expect(res4.body && res4.body.error)
139+
.toStrictEqual('Permission is not allowed: data');
140+
141+
const res5 = await request(app)
142+
.post('/cubejs-api/v1/sql')
143+
.set('Content-type', 'application/json')
144+
.set('Authorization', AUTH_TOKEN)
145+
.expect(403);
146+
147+
expect(res5.body && res5.body.error)
148+
.toStrictEqual('Permission is not allowed: data');
149+
150+
const res6 = await request(app)
151+
.get('/cubejs-api/v1/dry-run')
152+
.set('Authorization', AUTH_TOKEN)
153+
.expect(403);
154+
155+
expect(res6.body && res6.body.error)
156+
.toStrictEqual('Permission is not allowed: data');
157+
158+
const res7 = await request(app)
159+
.post('/cubejs-api/v1/dry-run')
160+
.set('Content-type', 'application/json')
161+
.set('Authorization', AUTH_TOKEN)
162+
.expect(403);
163+
164+
expect(res7.body && res7.body.error)
165+
.toStrictEqual('Permission is not allowed: data');
166+
167+
apiGateway.release();
168+
});
169+
170+
test('Jobs declined', async () => {
171+
const { app, apiGateway } = createApiGateway({
172+
contextToPermissions: async () => new Promise((resolve) => {
173+
resolve(['liveliness', 'graphql', 'data', 'meta']);
174+
}),
175+
});
176+
177+
const res1 = await request(app)
178+
.post('/cubejs-api/v1/pre-aggregations/jobs')
179+
.set('Authorization', AUTH_TOKEN)
180+
.expect(403);
181+
182+
expect(res1.body && res1.body.error)
183+
.toStrictEqual('Permission is not allowed: jobs');
184+
185+
const res2 = await request(app)
186+
.get('/cubejs-api/v1/run-scheduled-refresh')
187+
.set('Authorization', AUTH_TOKEN)
188+
.expect(403);
189+
190+
expect(res2.body && res2.body.error)
191+
.toStrictEqual('Permission is not allowed: jobs');
192+
193+
apiGateway.release();
194+
});
195+
});

packages/cubejs-server-core/src/core/optionsValidate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const schemaOptions = Joi.object().keys({
6464
contextToAppId: Joi.func(),
6565
contextToOrchestratorId: Joi.func(),
6666
contextToDataSourceId: Joi.func(),
67+
contextToPermissions: Joi.func(),
6768
repositoryFactory: Joi.func(),
6869
checkAuth: Joi.func(),
6970
checkAuthMiddleware: Joi.func(),

packages/cubejs-server-core/src/core/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ export class CubejsServerCore {
456456
scheduledRefreshContexts: this.options.scheduledRefreshContexts,
457457
scheduledRefreshTimeZones: this.options.scheduledRefreshTimeZones,
458458
serverCoreVersion: this.coreServerVersion,
459+
contextToPermissions: this.options.contextToPermissions,
459460
}
460461
));
461462
}

packages/cubejs-server-core/src/core/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
QueryRewriteFn,
99
CheckSQLAuthFn,
1010
CanSwitchSQLUserFn,
11+
ContextToPermissionsFn,
1112
} from '@cubejs-backend/api-gateway';
1213
import { BaseDriver, RedisPoolOptions, CacheAndQueryDriverType } from '@cubejs-backend/query-orchestrator';
1314
import { BaseQuery } from '@cubejs-backend/schema-compiler';
@@ -173,6 +174,7 @@ export interface CreateOptions {
173174
cacheAndQueueDriver?: CacheAndQueryDriverType;
174175
contextToAppId?: ContextToAppIdFn;
175176
contextToOrchestratorId?: ContextToOrchestratorIdFn;
177+
contextToPermissions?: ContextToPermissionsFn;
176178
repositoryFactory?: (context: RequestContext) => SchemaFileRepository;
177179
checkAuthMiddleware?: CheckAuthMiddlewareFn;
178180
checkAuth?: CheckAuthFn;

0 commit comments

Comments
 (0)