Skip to content

Commit

Permalink
WIP: new style access policy framework for Cube
Browse files Browse the repository at this point in the history
  • Loading branch information
bsod90 committed Oct 2, 2024
1 parent db2256d commit 9284272
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 21 deletions.
25 changes: 16 additions & 9 deletions packages/cubejs-api-gateway/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ class ApiGateway {
const { query, variables } = req.body;
const compilerApi = await this.getCompilerApi(req.context);

const metaConfig = await compilerApi.metaConfig({
const metaConfig = await compilerApi.metaConfig(req.context, {
requestId: req.context.requestId,
});

Expand Down Expand Up @@ -273,7 +273,7 @@ class ApiGateway {
const compilerApi = await this.getCompilerApi(req.context);
let schema = compilerApi.getGraphQLSchema();
if (!schema) {
let metaConfig = await compilerApi.metaConfig({
let metaConfig = await compilerApi.metaConfig(req.context, {
requestId: req.context.requestId,
});
metaConfig = this.filterVisibleItemsInMeta(req.context, metaConfig);
Expand Down Expand Up @@ -564,6 +564,7 @@ class ApiGateway {
private filterVisibleItemsInMeta(context: RequestContext, cubes: any[]) {
const isDevMode = getEnv('devMode');
function visibilityFilter(item) {
console.log('visibilityFilter', item, isDevMode, context.signedWithPlaygroundAuthSecret, item.isVisible);
return isDevMode || context.signedWithPlaygroundAuthSecret || item.isVisible;
}

Expand All @@ -589,7 +590,7 @@ class ApiGateway {
try {
await this.assertApiScope('meta', context.securityContext);
const compilerApi = await this.getCompilerApi(context);
const metaConfig = await compilerApi.metaConfig({
const metaConfig = await compilerApi.metaConfig(context, {
requestId: context.requestId,
includeCompilerId: includeCompilerId || onlyCompilerId
});
Expand Down Expand Up @@ -625,7 +626,7 @@ class ApiGateway {
try {
await this.assertApiScope('meta', context.securityContext);
const compilerApi = await this.getCompilerApi(context);
const metaConfigExtended = await compilerApi.metaConfigExtended({
const metaConfigExtended = await compilerApi.metaConfigExtended(context, {
requestId: context.requestId,
});
const { metaConfig, cubeDefinitions } = metaConfigExtended;
Expand Down Expand Up @@ -1048,7 +1049,7 @@ class ApiGateway {
} else {
const metaCacheKey = JSON.stringify(ctx);
if (!metaCache.has(metaCacheKey)) {
metaCache.set(metaCacheKey, await compiler.metaConfigExtended(ctx));
metaCache.set(metaCacheKey, await compiler.metaConfigExtended(context, ctx));
}

// checking and fetching result status
Expand Down Expand Up @@ -1233,8 +1234,14 @@ class ApiGateway {
}

const normalizedQuery = normalizeQuery(currentQuery, persistent);
let rewrittenQuery = await this.queryRewrite(
// First apply cube/view level security policies
let rewrittenQuery = (await this.compilerApi(context)).applyRowLevelSecurity(
normalizedQuery,
context
);
// Then apply user-supplied queryRewrite
rewrittenQuery = await this.queryRewrite(
rewrittenQuery,
context,
);

Expand Down Expand Up @@ -1552,7 +1559,7 @@ class ApiGateway {
if (normalizedQuery.total) {
const normalizedTotal = structuredClone(normalizedQuery);
normalizedTotal.totalQuery = true;

delete normalizedTotal.order;

normalizedTotal.limit = null;
Expand Down Expand Up @@ -1731,7 +1738,7 @@ class ApiGateway {
await this.getNormalizedQueries(query, context);

let metaConfigResult = await (await this
.getCompilerApi(context)).metaConfig({
.getCompilerApi(context)).metaConfig(request.context, {
requestId: context.requestId
});

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

const compilerApi = await this.getCompilerApi(context);
let metaConfigResult = await compilerApi.metaConfig({
let metaConfigResult = await compilerApi.metaConfig(request.context, {
requestId: context.requestId
});

Expand Down
84 changes: 84 additions & 0 deletions packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(
const CONTEXT_SYMBOLS = {
USER_CONTEXT: 'securityContext',
SECURITY_CONTEXT: 'securityContext',
security_context: 'securityContext',
FILTER_PARAMS: 'filterParams',
FILTER_GROUP: 'filterGroup',
SQL_UTILS: 'sqlUtils'
Expand Down Expand Up @@ -140,6 +141,7 @@ export class CubeSymbols {
this.camelCaseTypes(cube.dimensions);
this.camelCaseTypes(cube.segments);
this.camelCaseTypes(cube.preAggregations);
this.camelCaseTypes(cube.accessPolicy);

if (cube.preAggregations) {
this.transformPreAggregations(cube.preAggregations);
Expand All @@ -149,6 +151,10 @@ export class CubeSymbols {
this.prepareIncludes(cube, errorReporter, splitViews);
}

if (cube.accessPolicy) {
this.prepareAccessPolicy(cube, errorReporter);
}

return Object.assign(
{ cubeName: () => cube.name, cubeObj: () => cube },
cube.measures || {},
Expand Down Expand Up @@ -214,6 +220,59 @@ export class CubeSymbols {
}
}

/**
* @protected
*/
allMembersOrList(cube, specifier) {
const types = ['measures', 'dimensions'];
if (specifier === '*') {
const allMembers = R.unnest(types.map(type => Object.keys(cube[type] || {})));
console.log('allMembers', allMembers);
return allMembers;
} else {
return specifier || [];
}
}

/**
* @protected
*/
prepareAccessPolicy(cube, errorReporter) {
for (const policy of cube.accessPolicy) {
for (const filter of policy?.rowLevel?.filters || []) {
filter.memberReference = this.evaluateReferences(cube, filter.member);
if (filter.memberReference.indexOf('.') !== -1) {
errorReporter.error(
`Paths aren't allowed in security policy filters but '${filter.memberReference}' provided as member for ${cube.name}`
);
}
filter.memberReference = this.pathFromArray([cube.name, filter.memberReference]);
}

if (policy.memberLevel) {
const memberMapper = (member) => {
if (member.indexOf('.') !== -1) {
errorReporter.error(
`Paths aren't allowed in memberLevel policy but '${member}' provided as a member for ${cube.name}`
);
}
return this.pathFromArray([cube.name, member]);
};

const evaluatedIncludes = this.evaluateReferences(cube, policy.memberLevel.includes);
const evaluatedExcludes = this.evaluateReferences(cube, policy.memberLevel.excludes);

// TODO(maxim): Should includes be '*' by default or must it be explicitly defined?
if (!evaluatedIncludes) {
errorReporter.error(`${cube.name} memberLevel.includes must be defined or set to "*"`);
}

policy.memberLevel.includesMembers = this.allMembersOrList(cube, evaluatedIncludes).map(memberMapper);
policy.memberLevel.excludesMembers = this.allMembersOrList(cube, evaluatedExcludes).map(memberMapper);
}
}
}

/**
* @protected
*/
Expand Down Expand Up @@ -406,6 +465,27 @@ export class CubeSymbols {
});
}

// Used to evaluate access policies to allow referencing security_context at query time
evaluateContextFunction(cube, contextFn, context = {}) {
const cubeEvaluator = this;

const res = cubeEvaluator.resolveSymbolsCall(contextFn, (name) => {
const resolvedSymbol = this.resolveSymbol(cube, name);
if (resolvedSymbol) {
return resolvedSymbol;
}
throw new UserError(
`Cube references are not allowed when evaluating RLS conditions or filters. Found: ${name} in ${cube.name}`
);
}, {
contextSymbols: {
securityContext: context.securityContext,
}
});

return res;
}

evaluateReferences(cube, referencesFn, options = {}) {
const cubeEvaluator = this;

Expand Down Expand Up @@ -458,6 +538,10 @@ export class CubeSymbols {
res = res.fn.apply(null, res.memberNames.map((id) => nameResolver(id.trim())));
}
return res;
} catch (e) {
// TODO(maxim): should we keep this log?
console.log('Error while resolving Cube symbols: ', e);
console.error(e);
} finally {
this.resolveSymbolsCallContext = oldContext;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class CubeToMetaTransformer {
*/
transform(cube) {
const cubeTitle = cube.title || this.titleize(cube.name);

const isCubeVisible = this.isVisible(cube, true);

return {
Expand Down Expand Up @@ -95,6 +95,7 @@ export class CubeToMetaTransformer {
})),
R.toPairs
)(cube.segments || {}),
accessPolicy: cube.accessPolicy || [],
hierarchies: cube.hierarchies || []
},
};
Expand Down
56 changes: 56 additions & 0 deletions packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,61 @@ const SegmentsSchema = Joi.object().pattern(identifierRegex, Joi.object().keys({
public: Joi.boolean().strict(),
}));

const PolicyFilterSchema = Joi.object().keys({
member: Joi.func().required(),
memberReference: Joi.string().required(),
operator: Joi.any().valid(
'equals',
'notEquals',
'contains',
'notContains',
'startsWith',
'notStartsWith',
'endsWith',
'notEndsWith',
'gt',
'gte',
'lt',
'lte',
'inDateRange',
'notInDateRange',
'beforeDate',
'beforeOrOnDate',
'afterDate',
'afterOrOnDate',
).required(),
values: Joi.func().required(),
});

const MemberLevelPolicySchema = Joi.object().keys({
excludes: Joi.func(),
includes: Joi.func(),
includesMembers: Joi.array().items(Joi.string().required()),
excludesMembers: Joi.array().items(Joi.string().required()),
});

const RowLevelPolicySchema = Joi.object().keys({

filters: Joi.array().items(Joi.alternatives().try(
Joi.object().keys({
or: Joi.array().items(PolicyFilterSchema).required(),
and: Joi.array().items(PolicyFilterSchema).required(),
}),
PolicyFilterSchema,
)).required(),
});

// TODO(maxim): follow the "ATTENTION" thing below
const RolePolicySchema = Joi.object().keys({
role: Joi.string().required(),
memberLevel: MemberLevelPolicySchema,
rowLevel: RowLevelPolicySchema,
conditions: Joi.array().items(Joi.object().keys({
if: Joi.func().required(),
})),
// evaluatedConditions: Joi.array().items(Joi.boolean()),
});

/* *****************************
* ATTENTION:
* In case of adding/removing/changing any Joi.func() field that needs to be transpiled,
Expand Down Expand Up @@ -631,6 +686,7 @@ const baseSchema = {
title: Joi.string(),
levels: Joi.func()
})),
accessPolicy: Joi.array().items(RolePolicySchema),
};

const cubeSchema = inherit(baseSchema, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const transpiledFieldsPatterns: Array<RegExp> = [
/^excludes$/,
/^hierarchies\.[0-9]+\.levels$/,
/^cubes\.[0-9]+\.(joinPath|join_path)$/,
/^accessPolicy|access_policy\.[0-9]+\.rowLevel|row_level\.filters\.[0-9]+\.member$/,
/^accessPolicy|access_policy\.[0-9]+\.rowLevel|row_level\.filters\.[0-9]+\.values$/,
/^accessPolicy|access_policy\.[0-9]+\.conditions.[0-9]+\.if$/,
/^accessPolicy|access_policy\.[0-9]+\.memberLevel|member_level\.includes|excludes$/,
];

export const transpiledFields: Set<String> = new Set<String>();
Expand Down
Loading

0 comments on commit 9284272

Please sign in to comment.