Skip to content

Commit 9284272

Browse files
committed
WIP: new style access policy framework for Cube
1 parent db2256d commit 9284272

File tree

7 files changed

+341
-21
lines changed

7 files changed

+341
-21
lines changed

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ class ApiGateway {
240240
const { query, variables } = req.body;
241241
const compilerApi = await this.getCompilerApi(req.context);
242242

243-
const metaConfig = await compilerApi.metaConfig({
243+
const metaConfig = await compilerApi.metaConfig(req.context, {
244244
requestId: req.context.requestId,
245245
});
246246

@@ -273,7 +273,7 @@ class ApiGateway {
273273
const compilerApi = await this.getCompilerApi(req.context);
274274
let schema = compilerApi.getGraphQLSchema();
275275
if (!schema) {
276-
let metaConfig = await compilerApi.metaConfig({
276+
let metaConfig = await compilerApi.metaConfig(req.context, {
277277
requestId: req.context.requestId,
278278
});
279279
metaConfig = this.filterVisibleItemsInMeta(req.context, metaConfig);
@@ -564,6 +564,7 @@ class ApiGateway {
564564
private filterVisibleItemsInMeta(context: RequestContext, cubes: any[]) {
565565
const isDevMode = getEnv('devMode');
566566
function visibilityFilter(item) {
567+
console.log('visibilityFilter', item, isDevMode, context.signedWithPlaygroundAuthSecret, item.isVisible);
567568
return isDevMode || context.signedWithPlaygroundAuthSecret || item.isVisible;
568569
}
569570

@@ -589,7 +590,7 @@ class ApiGateway {
589590
try {
590591
await this.assertApiScope('meta', context.securityContext);
591592
const compilerApi = await this.getCompilerApi(context);
592-
const metaConfig = await compilerApi.metaConfig({
593+
const metaConfig = await compilerApi.metaConfig(context, {
593594
requestId: context.requestId,
594595
includeCompilerId: includeCompilerId || onlyCompilerId
595596
});
@@ -625,7 +626,7 @@ class ApiGateway {
625626
try {
626627
await this.assertApiScope('meta', context.securityContext);
627628
const compilerApi = await this.getCompilerApi(context);
628-
const metaConfigExtended = await compilerApi.metaConfigExtended({
629+
const metaConfigExtended = await compilerApi.metaConfigExtended(context, {
629630
requestId: context.requestId,
630631
});
631632
const { metaConfig, cubeDefinitions } = metaConfigExtended;
@@ -1048,7 +1049,7 @@ class ApiGateway {
10481049
} else {
10491050
const metaCacheKey = JSON.stringify(ctx);
10501051
if (!metaCache.has(metaCacheKey)) {
1051-
metaCache.set(metaCacheKey, await compiler.metaConfigExtended(ctx));
1052+
metaCache.set(metaCacheKey, await compiler.metaConfigExtended(context, ctx));
10521053
}
10531054

10541055
// checking and fetching result status
@@ -1233,8 +1234,14 @@ class ApiGateway {
12331234
}
12341235

12351236
const normalizedQuery = normalizeQuery(currentQuery, persistent);
1236-
let rewrittenQuery = await this.queryRewrite(
1237+
// First apply cube/view level security policies
1238+
let rewrittenQuery = (await this.compilerApi(context)).applyRowLevelSecurity(
12371239
normalizedQuery,
1240+
context
1241+
);
1242+
// Then apply user-supplied queryRewrite
1243+
rewrittenQuery = await this.queryRewrite(
1244+
rewrittenQuery,
12381245
context,
12391246
);
12401247

@@ -1552,7 +1559,7 @@ class ApiGateway {
15521559
if (normalizedQuery.total) {
15531560
const normalizedTotal = structuredClone(normalizedQuery);
15541561
normalizedTotal.totalQuery = true;
1555-
1562+
15561563
delete normalizedTotal.order;
15571564

15581565
normalizedTotal.limit = null;
@@ -1731,7 +1738,7 @@ class ApiGateway {
17311738
await this.getNormalizedQueries(query, context);
17321739

17331740
let metaConfigResult = await (await this
1734-
.getCompilerApi(context)).metaConfig({
1741+
.getCompilerApi(context)).metaConfig(request.context, {
17351742
requestId: context.requestId
17361743
});
17371744

@@ -1841,7 +1848,7 @@ class ApiGateway {
18411848
await this.getNormalizedQueries(query, context, request.streaming, request.memberExpressions);
18421849

18431850
const compilerApi = await this.getCompilerApi(context);
1844-
let metaConfigResult = await compilerApi.metaConfig({
1851+
let metaConfigResult = await compilerApi.metaConfig(request.context, {
18451852
requestId: context.requestId
18461853
});
18471854

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(
1111
const CONTEXT_SYMBOLS = {
1212
USER_CONTEXT: 'securityContext',
1313
SECURITY_CONTEXT: 'securityContext',
14+
security_context: 'securityContext',
1415
FILTER_PARAMS: 'filterParams',
1516
FILTER_GROUP: 'filterGroup',
1617
SQL_UTILS: 'sqlUtils'
@@ -140,6 +141,7 @@ export class CubeSymbols {
140141
this.camelCaseTypes(cube.dimensions);
141142
this.camelCaseTypes(cube.segments);
142143
this.camelCaseTypes(cube.preAggregations);
144+
this.camelCaseTypes(cube.accessPolicy);
143145

144146
if (cube.preAggregations) {
145147
this.transformPreAggregations(cube.preAggregations);
@@ -149,6 +151,10 @@ export class CubeSymbols {
149151
this.prepareIncludes(cube, errorReporter, splitViews);
150152
}
151153

154+
if (cube.accessPolicy) {
155+
this.prepareAccessPolicy(cube, errorReporter);
156+
}
157+
152158
return Object.assign(
153159
{ cubeName: () => cube.name, cubeObj: () => cube },
154160
cube.measures || {},
@@ -214,6 +220,59 @@ export class CubeSymbols {
214220
}
215221
}
216222

223+
/**
224+
* @protected
225+
*/
226+
allMembersOrList(cube, specifier) {
227+
const types = ['measures', 'dimensions'];
228+
if (specifier === '*') {
229+
const allMembers = R.unnest(types.map(type => Object.keys(cube[type] || {})));
230+
console.log('allMembers', allMembers);
231+
return allMembers;
232+
} else {
233+
return specifier || [];
234+
}
235+
}
236+
237+
/**
238+
* @protected
239+
*/
240+
prepareAccessPolicy(cube, errorReporter) {
241+
for (const policy of cube.accessPolicy) {
242+
for (const filter of policy?.rowLevel?.filters || []) {
243+
filter.memberReference = this.evaluateReferences(cube, filter.member);
244+
if (filter.memberReference.indexOf('.') !== -1) {
245+
errorReporter.error(
246+
`Paths aren't allowed in security policy filters but '${filter.memberReference}' provided as member for ${cube.name}`
247+
);
248+
}
249+
filter.memberReference = this.pathFromArray([cube.name, filter.memberReference]);
250+
}
251+
252+
if (policy.memberLevel) {
253+
const memberMapper = (member) => {
254+
if (member.indexOf('.') !== -1) {
255+
errorReporter.error(
256+
`Paths aren't allowed in memberLevel policy but '${member}' provided as a member for ${cube.name}`
257+
);
258+
}
259+
return this.pathFromArray([cube.name, member]);
260+
};
261+
262+
const evaluatedIncludes = this.evaluateReferences(cube, policy.memberLevel.includes);
263+
const evaluatedExcludes = this.evaluateReferences(cube, policy.memberLevel.excludes);
264+
265+
// TODO(maxim): Should includes be '*' by default or must it be explicitly defined?
266+
if (!evaluatedIncludes) {
267+
errorReporter.error(`${cube.name} memberLevel.includes must be defined or set to "*"`);
268+
}
269+
270+
policy.memberLevel.includesMembers = this.allMembersOrList(cube, evaluatedIncludes).map(memberMapper);
271+
policy.memberLevel.excludesMembers = this.allMembersOrList(cube, evaluatedExcludes).map(memberMapper);
272+
}
273+
}
274+
}
275+
217276
/**
218277
* @protected
219278
*/
@@ -406,6 +465,27 @@ export class CubeSymbols {
406465
});
407466
}
408467

468+
// Used to evaluate access policies to allow referencing security_context at query time
469+
evaluateContextFunction(cube, contextFn, context = {}) {
470+
const cubeEvaluator = this;
471+
472+
const res = cubeEvaluator.resolveSymbolsCall(contextFn, (name) => {
473+
const resolvedSymbol = this.resolveSymbol(cube, name);
474+
if (resolvedSymbol) {
475+
return resolvedSymbol;
476+
}
477+
throw new UserError(
478+
`Cube references are not allowed when evaluating RLS conditions or filters. Found: ${name} in ${cube.name}`
479+
);
480+
}, {
481+
contextSymbols: {
482+
securityContext: context.securityContext,
483+
}
484+
});
485+
486+
return res;
487+
}
488+
409489
evaluateReferences(cube, referencesFn, options = {}) {
410490
const cubeEvaluator = this;
411491

@@ -458,6 +538,10 @@ export class CubeSymbols {
458538
res = res.fn.apply(null, res.memberNames.map((id) => nameResolver(id.trim())));
459539
}
460540
return res;
541+
} catch (e) {
542+
// TODO(maxim): should we keep this log?
543+
console.log('Error while resolving Cube symbols: ', e);
544+
console.error(e);
461545
} finally {
462546
this.resolveSymbolsCallContext = oldContext;
463547
}

packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class CubeToMetaTransformer {
3939
*/
4040
transform(cube) {
4141
const cubeTitle = cube.title || this.titleize(cube.name);
42-
42+
4343
const isCubeVisible = this.isVisible(cube, true);
4444

4545
return {
@@ -95,6 +95,7 @@ export class CubeToMetaTransformer {
9595
})),
9696
R.toPairs
9797
)(cube.segments || {}),
98+
accessPolicy: cube.accessPolicy || [],
9899
hierarchies: cube.hierarchies || []
99100
},
100101
};

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,61 @@ const SegmentsSchema = Joi.object().pattern(identifierRegex, Joi.object().keys({
554554
public: Joi.boolean().strict(),
555555
}));
556556

557+
const PolicyFilterSchema = Joi.object().keys({
558+
member: Joi.func().required(),
559+
memberReference: Joi.string().required(),
560+
operator: Joi.any().valid(
561+
'equals',
562+
'notEquals',
563+
'contains',
564+
'notContains',
565+
'startsWith',
566+
'notStartsWith',
567+
'endsWith',
568+
'notEndsWith',
569+
'gt',
570+
'gte',
571+
'lt',
572+
'lte',
573+
'inDateRange',
574+
'notInDateRange',
575+
'beforeDate',
576+
'beforeOrOnDate',
577+
'afterDate',
578+
'afterOrOnDate',
579+
).required(),
580+
values: Joi.func().required(),
581+
});
582+
583+
const MemberLevelPolicySchema = Joi.object().keys({
584+
excludes: Joi.func(),
585+
includes: Joi.func(),
586+
includesMembers: Joi.array().items(Joi.string().required()),
587+
excludesMembers: Joi.array().items(Joi.string().required()),
588+
});
589+
590+
const RowLevelPolicySchema = Joi.object().keys({
591+
592+
filters: Joi.array().items(Joi.alternatives().try(
593+
Joi.object().keys({
594+
or: Joi.array().items(PolicyFilterSchema).required(),
595+
and: Joi.array().items(PolicyFilterSchema).required(),
596+
}),
597+
PolicyFilterSchema,
598+
)).required(),
599+
});
600+
601+
// TODO(maxim): follow the "ATTENTION" thing below
602+
const RolePolicySchema = Joi.object().keys({
603+
role: Joi.string().required(),
604+
memberLevel: MemberLevelPolicySchema,
605+
rowLevel: RowLevelPolicySchema,
606+
conditions: Joi.array().items(Joi.object().keys({
607+
if: Joi.func().required(),
608+
})),
609+
// evaluatedConditions: Joi.array().items(Joi.boolean()),
610+
});
611+
557612
/* *****************************
558613
* ATTENTION:
559614
* In case of adding/removing/changing any Joi.func() field that needs to be transpiled,
@@ -631,6 +686,7 @@ const baseSchema = {
631686
title: Joi.string(),
632687
levels: Joi.func()
633688
})),
689+
accessPolicy: Joi.array().items(RolePolicySchema),
634690
};
635691

636692
const cubeSchema = inherit(baseSchema, {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const transpiledFieldsPatterns: Array<RegExp> = [
2525
/^excludes$/,
2626
/^hierarchies\.[0-9]+\.levels$/,
2727
/^cubes\.[0-9]+\.(joinPath|join_path)$/,
28+
/^accessPolicy|access_policy\.[0-9]+\.rowLevel|row_level\.filters\.[0-9]+\.member$/,
29+
/^accessPolicy|access_policy\.[0-9]+\.rowLevel|row_level\.filters\.[0-9]+\.values$/,
30+
/^accessPolicy|access_policy\.[0-9]+\.conditions.[0-9]+\.if$/,
31+
/^accessPolicy|access_policy\.[0-9]+\.memberLevel|member_level\.includes|excludes$/,
2832
];
2933

3034
export const transpiledFields: Set<String> = new Set<String>();

0 commit comments

Comments
 (0)