Skip to content

Commit 8a13051

Browse files
authored
feat(schema-compiler): groups support (#9903)
* feat: introduce groups support * wip * wip * updated snapshots
1 parent b6e0699 commit 8a13051

File tree

15 files changed

+298
-24
lines changed

15 files changed

+298
-24
lines changed

packages/cubejs-backend-native/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ export interface PyConfiguration {
527527
scheduledRefreshContexts?: (ctx: unknown) => Promise<string[]>
528528
scheduledRefreshTimeZones?: (ctx: unknown) => Promise<string[]>
529529
contextToRoles?: (ctx: unknown) => Promise<string[]>
530+
contextToGroups?: (ctx: unknown) => Promise<string[]>
530531
}
531532

532533
function simplifyExpressRequest(req: ExpressRequest) {

packages/cubejs-backend-native/python/cube/src/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class Configuration:
7878
pre_aggregations_schema: Union[Callable[[RequestContext], str], str]
7979
orchestrator_options: Union[Dict, Callable[[RequestContext], Dict]]
8080
context_to_roles: Callable[[RequestContext], list[str]]
81+
context_to_groups: Callable[[RequestContext], list[str]]
8182
fast_reload: bool
8283

8384
def __init__(self):
@@ -128,6 +129,7 @@ def __init__(self):
128129
self.pre_aggregations_schema = None
129130
self.orchestrator_options = None
130131
self.context_to_roles = None
132+
self.context_to_groups = None
131133
self.fast_reload = None
132134

133135
def __call__(self, func):

packages/cubejs-backend-native/src/python/cube_config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ impl CubeConfigPy {
5252
"context_to_orchestrator_id",
5353
"context_to_cube_store_router_id",
5454
"context_to_roles",
55+
"context_to_groups",
5556
"db_type",
5657
"driver_factory",
5758
"extend_context",

packages/cubejs-backend-native/test/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,13 @@ def context_to_roles(ctx):
106106
return [
107107
"admin",
108108
]
109+
110+
111+
@config
112+
def context_to_groups(ctx):
113+
print("[python] context_to_groups", ctx)
114+
115+
return [
116+
"dev",
117+
"analytics",
118+
]

packages/cubejs-backend-native/test/old-config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,15 @@ def logger(msg, params):
7878
print('[python] logger msg', msg, 'params=', params)
7979

8080
settings.logger = logger
81+
82+
def context_to_roles(ctx):
83+
print('[python] context_to_roles', ctx)
84+
return ['admin']
85+
86+
settings.context_to_roles = context_to_roles
87+
88+
def context_to_groups(ctx):
89+
print('[python] context_to_groups', ctx)
90+
return ['dev', 'analytics']
91+
92+
settings.context_to_groups = context_to_groups

packages/cubejs-backend-native/test/python.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ suite('Python Config', () => {
6969
repositoryFactory: expect.any(Function),
7070
schemaVersion: expect.any(Function),
7171
contextToRoles: expect.any(Function),
72+
contextToGroups: expect.any(Function),
7273
scheduledRefreshContexts: expect.any(Function),
7374
scheduledRefreshTimeZones: expect.any(Function),
7475
});
@@ -99,6 +100,14 @@ suite('Python Config', () => {
99100
expect(await config.contextToRoles({})).toEqual(['admin']);
100101
});
101102

103+
test('context_to_groups', async () => {
104+
if (!config.contextToGroups) {
105+
throw new Error('contextToGroups was not defined in config.py');
106+
}
107+
108+
expect(await config.contextToGroups({})).toEqual(['dev', 'analytics']);
109+
});
110+
102111
test('context_to_api_scopes', async () => {
103112
if (!config.contextToApiScopes) {
104113
throw new Error('contextToApiScopes was not defined in config.py');
@@ -243,6 +252,7 @@ darwinSuite('Old Python Config', () => {
243252
repositoryFactory: expect.any(Function),
244253
schemaVersion: expect.any(Function),
245254
contextToRoles: expect.any(Function),
255+
contextToGroups: expect.any(Function),
246256
scheduledRefreshContexts: expect.any(Function),
247257
scheduledRefreshTimeZones: expect.any(Function),
248258
});

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export type Filter =
118118
};
119119

120120
export type AccessPolicyDefinition = {
121+
role?: string;
122+
group?: string;
123+
groups?: string[];
121124
rowLevel?: {
122125
filters: Filter[];
123126
};

packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -765,13 +765,19 @@ const RowLevelPolicySchema = Joi.object().keys({
765765
}).xor('filters', 'allowAll');
766766

767767
const RolePolicySchema = Joi.object().keys({
768-
role: Joi.string().required(),
768+
role: Joi.string(),
769+
group: Joi.string(),
770+
groups: Joi.array().items(Joi.string()),
769771
memberLevel: MemberLevelPolicySchema,
770772
rowLevel: RowLevelPolicySchema,
771773
conditions: Joi.array().items(Joi.object().keys({
772774
if: Joi.func().required(),
773775
})),
774-
});
776+
})
777+
.nand('group', 'groups') // Cannot have both group and groups
778+
.nand('role', 'group') // Cannot have both role and group
779+
.nand('role', 'groups') // Cannot have both role and groups
780+
.or('role', 'group', 'groups'); // Must have at least one
775781

776782
/* *****************************
777783
* ATTENTION:

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import {
88
TranspilerSymbolResolver,
99
TraverseObject
1010
} from './transpiler.interface';
11-
import type { CubeSymbols } from '../CubeSymbols';
12-
import type { CubeDictionary } from '../CubeDictionary';
1311

1412
/* this list was generated by getTransformPatterns() with additional variants for snake_case */
1513
export const transpiledFieldsPatterns: Array<RegExp> = [

packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,4 +1166,130 @@ describe('Cube Validation', () => {
11661166
}
11671167
});
11681168
});
1169+
1170+
describe('Access Policy group/groups support:', () => {
1171+
const cubeValidator = new CubeValidator(new CubeSymbols());
1172+
1173+
it('should allow group instead of role', () => {
1174+
const cube = {
1175+
name: 'TestCube',
1176+
fileName: 'test.js',
1177+
sql: () => 'SELECT * FROM test',
1178+
accessPolicy: [{
1179+
group: 'admin',
1180+
rowLevel: { allowAll: true }
1181+
}]
1182+
};
1183+
1184+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1185+
expect(result.error).toBeFalsy();
1186+
});
1187+
1188+
it('should allow groups as array', () => {
1189+
const cube = {
1190+
name: 'TestCube',
1191+
fileName: 'test.js',
1192+
sql: () => 'SELECT * FROM test',
1193+
accessPolicy: [{
1194+
groups: ['admin', 'user'],
1195+
rowLevel: { allowAll: true }
1196+
}]
1197+
};
1198+
1199+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1200+
expect(result.error).toBeFalsy();
1201+
});
1202+
1203+
it('should allow role as single string (existing behavior)', () => {
1204+
const cube = {
1205+
name: 'TestCube',
1206+
fileName: 'test.js',
1207+
sql: () => 'SELECT * FROM test',
1208+
accessPolicy: [{
1209+
role: 'admin',
1210+
rowLevel: { allowAll: true }
1211+
}]
1212+
};
1213+
1214+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1215+
expect(result.error).toBeFalsy();
1216+
});
1217+
1218+
it('should allow group: "*" syntax', () => {
1219+
const cube = {
1220+
name: 'TestCube',
1221+
fileName: 'test.js',
1222+
sql: () => 'SELECT * FROM test',
1223+
accessPolicy: [{
1224+
group: '*',
1225+
rowLevel: { allowAll: true }
1226+
}]
1227+
};
1228+
1229+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1230+
expect(result.error).toBeFalsy();
1231+
});
1232+
1233+
it('should reject role and group together', () => {
1234+
const cube = {
1235+
name: 'TestCube',
1236+
fileName: 'test.js',
1237+
sql: () => 'SELECT * FROM test',
1238+
accessPolicy: [{
1239+
role: 'admin',
1240+
group: 'admin',
1241+
rowLevel: { allowAll: true }
1242+
}]
1243+
};
1244+
1245+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1246+
expect(result.error).toBeTruthy();
1247+
});
1248+
1249+
it('should reject role and groups together', () => {
1250+
const cube = {
1251+
name: 'TestCube',
1252+
fileName: 'test.js',
1253+
sql: () => 'SELECT * FROM test',
1254+
accessPolicy: [{
1255+
role: 'admin',
1256+
groups: ['user'],
1257+
rowLevel: { allowAll: true }
1258+
}]
1259+
};
1260+
1261+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1262+
expect(result.error).toBeTruthy();
1263+
});
1264+
1265+
it('should reject group and groups together', () => {
1266+
const cube = {
1267+
name: 'TestCube',
1268+
fileName: 'test.js',
1269+
sql: () => 'SELECT * FROM test',
1270+
accessPolicy: [{
1271+
group: 'admin',
1272+
groups: ['user'],
1273+
rowLevel: { allowAll: true }
1274+
}]
1275+
};
1276+
1277+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1278+
expect(result.error).toBeTruthy();
1279+
});
1280+
1281+
it('should reject access policy without role/group/groups', () => {
1282+
const cube = {
1283+
name: 'TestCube',
1284+
fileName: 'test.js',
1285+
sql: () => 'SELECT * FROM test',
1286+
accessPolicy: [{
1287+
rowLevel: { allowAll: true }
1288+
}]
1289+
};
1290+
1291+
const result = cubeValidator.validate(cube, new ConsoleErrorReporter());
1292+
expect(result.error).toBeTruthy();
1293+
});
1294+
});
11691295
});

0 commit comments

Comments
 (0)