diff --git a/.gitignore b/.gitignore index 0182bdb1a4d04..20be737ddce44 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ testings/ rust/cubesql/profile.json .cubestore .env - +.vimspector.json diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index a067bcc9d95ed..dc9b5a4f341a9 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -234,7 +234,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, }); @@ -267,7 +267,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); @@ -551,7 +551,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 }); @@ -587,7 +587,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; @@ -1010,7 +1010,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 @@ -1195,8 +1195,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, ); @@ -1693,7 +1699,7 @@ class ApiGateway { await this.getNormalizedQueries(query, context); let metaConfigResult = await (await this - .getCompilerApi(context)).metaConfig({ + .getCompilerApi(context)).metaConfig(request.context, { requestId: context.requestId }); @@ -1803,7 +1809,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 }); diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts index e1240f27d5ccf..6929e233f59f8 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts @@ -112,9 +112,71 @@ export class CubeEvaluator extends CubeSymbols { this.prepareHierarchies(cube); + this.prepareAccessPolicy(cube, errorReporter); + return cube; } + private allMembersOrList(cube: any, specifier: string | string[]): string[] { + const types = ['measures', 'dimensions']; + if (specifier === '*') { + const allMembers = R.unnest(types.map(type => Object.keys(cube[type] || {}))); + return allMembers; + } else { + return specifier as string[] || []; + } + } + + private prepareAccessPolicy(cube: any, errorReporter: ErrorReporter) { + if (!cube.accessPolicy) { + return; + } + + const memberMapper = (memberType: string) => (member: string) => { + if (member.indexOf('.') !== -1) { + const cubeName = member.split('.')[0]; + if (cubeName !== cube.name) { + errorReporter.error( + `Paths aren't allowed in the accessPolicy policy but '${member}' provided as ${memberType} for ${cube.name}` + ); + } + return member; + } + return this.pathFromArray([cube.name, member]); + }; + + const filterEvaluator = (filter: any) => { + if (filter.member) { + filter.memberReference = this.evaluateReferences(cube.name, filter.member); + filter.memberReference = memberMapper('a filter member reference')(filter.memberReference); + } else { + if (filter.and) { + filter.and.forEach(filterEvaluator); + } + if (filter.or) { + filter.or.forEach(filterEvaluator); + } + } + }; + + for (const policy of cube.accessPolicy) { + for (const filter of policy?.rowLevel?.filters || []) { + filterEvaluator(filter); + } + + if (policy.memberLevel) { + policy.memberLevel.includesMembers = this.allMembersOrList( + cube, + policy.memberLevel.includes + ).map(memberMapper('an includes member')); + policy.memberLevel.excludesMembers = this.allMembersOrList( + cube, + policy.memberLevel.excludes || [] + ).map(memberMapper('an excludes member')); + } + } + } + private prepareHierarchies(cube: any) { if (Array.isArray(cube.hierarchies)) { cube.hierarchies = cube.hierarchies.map(hierarchy => ({ diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js index 09b689697d0f5..4430a7bc21111 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js @@ -10,6 +10,7 @@ import { BaseQuery } from '../adapter'; const FunctionRegex = /function\s+\w+\(([A-Za-z0-9_,]*)|\(([\s\S]*?)\)\s*=>|\(?(\w+)\)?\s*=>/; const CONTEXT_SYMBOLS = { SECURITY_CONTEXT: 'securityContext', + security_context: 'securityContext', FILTER_PARAMS: 'filterParams', FILTER_GROUP: 'filterGroup', SQL_UTILS: 'sqlUtils' @@ -139,6 +140,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); @@ -406,6 +408,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; @@ -437,7 +460,7 @@ export class CubeSymbols { collectJoinHints: options.collectJoinHints, }); if (!Array.isArray(arrayOrSingle)) { - return arrayOrSingle.toString(); + return arrayOrSingle && arrayOrSingle.toString(); } const references = arrayOrSingle.map(p => p.toString()); @@ -458,6 +481,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; } diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js index 5235557c3c0a8..531e2ef4e54d1 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js @@ -105,6 +105,7 @@ export class CubeToMetaTransformer { })), R.toPairs )(cube.segments || {}), + accessPolicy: cube.accessPolicy || [], hierarchies: cube.hierarchies || [] }, }; diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 34a760ebd9699..17de55b16ca57 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -25,7 +25,8 @@ export const nonStringFields = new Set([ 'external', 'useOriginalSqlPreAggregations', 'readOnly', - 'prefix' + 'prefix', + 'if', ]); const identifierRegex = /^[_a-zA-Z][_a-zA-Z0-9]*$/; @@ -615,6 +616,63 @@ 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(), + 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 PolicyFilterConditionSchema = Joi.object().keys({ + or: Joi.array().items(PolicyFilterSchema, Joi.link('...').description('Filter Condition schema')), + and: Joi.array().items(PolicyFilterSchema, Joi.link('...').description('Filter Condition schema')), +}).xor('or', 'and'); + +const MemberLevelPolicySchema = Joi.object().keys({ + // TODO(maxim): these should be .func()? Should they even allow references? + includes: Joi.alternatives([ + Joi.string().valid('*'), + Joi.array().items(Joi.string().required()) + ]).required(), + excludes: Joi.alternatives([ + Joi.array().items(Joi.string().required()) + ]), + includesMembers: Joi.array().items(Joi.string().required()), + excludesMembers: Joi.array().items(Joi.string().required()), +}); + +const RowLevelPolicySchema = Joi.object().keys({ + filters: Joi.array().items(PolicyFilterSchema, PolicyFilterConditionSchema).required(), +}); + +const RolePolicySchema = Joi.object().keys({ + role: Joi.string().required(), + memberLevel: MemberLevelPolicySchema, + rowLevel: RowLevelPolicySchema, + conditions: Joi.array().items(Joi.object().keys({ + if: Joi.func().required(), + })), +}); + /* ***************************** * ATTENTION: * In case of adding/removing/changing any Joi.func() field that needs to be transpiled, @@ -692,6 +750,7 @@ const baseSchema = { title: Joi.string(), levels: Joi.func() })), + accessPolicy: Joi.array().items(RolePolicySchema), }; const cubeSchema = inherit(baseSchema, { diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index f55c71dca29c2..7ceea91fb687c 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -25,6 +25,10 @@ export const transpiledFieldsPatterns: Array = [ /^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 = new Set(); diff --git a/packages/cubejs-schema-compiler/src/compiler/utils.ts b/packages/cubejs-schema-compiler/src/compiler/utils.ts index 64380629e7883..aea98cd017b62 100644 --- a/packages/cubejs-schema-compiler/src/compiler/utils.ts +++ b/packages/cubejs-schema-compiler/src/compiler/utils.ts @@ -48,6 +48,7 @@ export function camelizeCube(cube: any): unknown { camelizeObjectPart(cube.dimensions, false); camelizeObjectPart(cube.preAggregations, false); camelizeObjectPart(cube.cubes, false); + camelizeObjectPart(cube.accessPolicy, false); return cube; } diff --git a/packages/cubejs-schema-compiler/test/unit/schema.test.ts b/packages/cubejs-schema-compiler/test/unit/schema.test.ts index ec3d4dc666356..16701c723b0eb 100644 --- a/packages/cubejs-schema-compiler/test/unit/schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/schema.test.ts @@ -1,5 +1,5 @@ import { prepareCompiler } from './PrepareCompiler'; -import { createCubeSchema, createCubeSchemaWithCustomGranularities } from './utils'; +import { createCubeSchema, createCubeSchemaWithCustomGranularities, createCubeSchemaWithAccessPolicy } from './utils'; describe('Schema Testing', () => { const schemaCompile = async () => { @@ -367,4 +367,15 @@ describe('Schema Testing', () => { CubeD: { relationship: 'belongsTo' } }); }); + + it('valid schema with accessPolicy', async () => { + const { compiler, metaTransformer } = prepareCompiler([ + createCubeSchemaWithAccessPolicy('ProtectedCube'), + ]); + await compiler.compile(); + compiler.throwIfAnyErrors(); + + // TODO(maxim): this should be further validated + expect(metaTransformer.cubes[0].config.accessPolicy).toBeDefined(); + }); }); diff --git a/packages/cubejs-schema-compiler/test/unit/utils.ts b/packages/cubejs-schema-compiler/test/unit/utils.ts index 969bf7f8cc280..50dbd8ac76ac7 100644 --- a/packages/cubejs-schema-compiler/test/unit/utils.ts +++ b/packages/cubejs-schema-compiler/test/unit/utils.ts @@ -96,6 +96,131 @@ export function createCubeSchema({ name, refreshKey = '', preAggregations = '', `; } +export function createCubeSchemaWithAccessPolicy(name: string, extraPolicies: string = ''): string { + return `cube('${name}', { + description: 'test cube from createCubeSchemaWithAccessPolicy', + sql: 'select * from cards', + + measures: { + count: { + description: 'count measure from createCubeSchemaWithAccessPolicy', + type: 'count' + }, + sum: { + sql: \`amount\`, + type: \`sum\` + }, + max: { + sql: \`amount\`, + type: \`max\` + }, + min: { + sql: \`amount\`, + type: \`min\` + }, + diff: { + sql: \`\${max} - \${min}\`, + type: \`number\` + } + }, + + dimensions: { + id: { + type: 'number', + description: 'id dimension from createCubeSchemaWithAccessPolicy', + sql: 'id', + primaryKey: true + }, + id_cube: { + type: 'number', + sql: \`\${CUBE}.id\`, + }, + other_id: { + type: 'number', + sql: 'other_id', + }, + type: { + type: 'string', + sql: 'type' + }, + type_with_cube: { + type: 'string', + sql: \`\${CUBE.type}\`, + }, + type_complex: { + type: 'string', + sql: \`CONCAT(\${type}, ' ', \${location})\`, + }, + createdAt: { + type: 'time', + sql: 'created_at' + }, + location: { + type: 'string', + sql: 'location' + } + }, + accessPolicy: [ + { + role: "*", + }, + { + role: 'admin', + conditions: [ + { + if: \`true\`, + } + ], + rowLevel: { + filters: [ + { + member: \`$\{CUBE}.id\`, + operator: 'equals', + values: [\`1\`, \`2\`, \`3\`] + } + ] + }, + memberLevel: { + includes: \`*\`, + excludes: [\`location\`, \`diff\`] + }, + }, + { + role: 'manager', + conditions: [ + { + if: security_context.userId === 1, + } + ], + rowLevel: { + filters: [ + { + or: [ + { + member: \`location\`, + operator: 'startsWith', + values: [\`San\`] + }, + { + member: \`location\`, + operator: 'startsWith', + values: [\`Lon\`] + } + ] + } + ] + }, + memberLevel: { + includes: \`*\`, + excludes: [\`min\`, \`max\`] + }, + }, + ${extraPolicies} + ] + }) + `; +} + export function createCubeSchemaWithCustomGranularities(name: string): string { return `cube('${name}', { sql: 'select * from orders', diff --git a/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts b/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts index f1e9742fb19b3..3823990c132bd 100644 --- a/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts @@ -345,6 +345,46 @@ describe('Yaml Schema Testing', () => { ` ); + await compiler.compile(); + }); + }); + describe('Access policy: ', () => { + it('defines a correct accessPolicy', async () => { + const { compiler } = prepareYamlCompiler( + ` + cubes: + - name: Orders + sql: "select * from tbl" + dimensions: + - name: created_at + sql: created_at + type: time + - name: status + sql: status + type: string + measures: + - name: count + type: count + accessPolicy: + - role: admin + conditions: + - if: "{ !security_context.isBlocked }" + rowLevel: + filters: + - member: status + operator: equals + values: ["completed"] + - member: "{CUBE}.created_at" + operator: notInDateRange + values: + - 2022-01-01 + - "{ security_context.currentDate }" + memberLevel: + includes: + - status + ` + ); + await compiler.compile(); }); }); diff --git a/packages/cubejs-server-core/src/core/CompilerApi.js b/packages/cubejs-server-core/src/core/CompilerApi.js index 401e50ac5d46c..6a7c6b4dd99ad 100644 --- a/packages/cubejs-server-core/src/core/CompilerApi.js +++ b/packages/cubejs-server-core/src/core/CompilerApi.js @@ -185,6 +185,111 @@ export class CompilerApi { } } + getRolesFromContext(context) { + const securityContext = (context && context.securityContext) || {}; + return new Set((securityContext.cloud && securityContext.cloud.roles) || []); + } + + userHasRole(userRoles, role) { + return userRoles.has(role) || role === '*'; + } + + roleMeetsConditions(evaluatedConditions) { + if (evaluatedConditions && evaluatedConditions.length) { + return evaluatedConditions.reduce((a, b) => { + if (typeof b !== 'boolean') { + throw new Error(`Access policy condition must return boolean, got ${JSON.stringify(b)}`); + } + return a || b; + }); + } + return true; + } + + async getCubesFromQuery(query) { + const sql = await this.getSql(query, { requestId: query.requestId }); + return new Set(sql.memberNames.map(memberName => memberName.split('.')[0])); + } + + getApplicablePolicies(cube, context, cubeEvaluator) { + const userRoles = this.getRolesFromContext(context); + return cube.accessPolicy.filter(policy => { + const evaluatedConditions = (policy.conditions || []).map( + condition => cubeEvaluator.evaluateContextFunction(cube, condition.if, context) + ); + const res = this.userHasRole(userRoles, policy.role) && this.roleMeetsConditions(evaluatedConditions); + return res; + }); + } + + rlsEnabledForCube(cube) { + return cube.accessPolicy && cube.accessPolicy.length && cube.accessPolicy.length > 0; + } + + isRlsEnabled(cubeEvaluator) { + return cubeEvaluator.cubeNames().some(cubeName => this.rlsEnabledForCube(cubeEvaluator.cubeFromPath(cubeName))); + } + + evaluateNestedFilter(filter, cube, context, cubeEvaluator) { + const result = { + }; + if (filter.memberReference) { + // TODO(maxim): will it work with different data types? + const evaluatedValues = cubeEvaluator.evaluateContextFunction( + cube, + filter.values, + context + ); + result.member = filter.memberReference; + result.operator = filter.operator; + result.values = evaluatedValues; + } + if (filter.or) { + result.or = filter.or.map(f => this.evaluateNestedFilter(f, cube, context, cubeEvaluator)); + } + if (filter.and) { + result.and = filter.and.map(f => this.evaluateNestedFilter(f, cube, context, cubeEvaluator)); + } + return result; + } + + async applyRowLevelSecurity(query, context) { + const { cubeEvaluator } = await this.getCompilers({ requestId: query.requestId }); + + if (!this.isRlsEnabled(cubeEvaluator)) { + return query; + } + const queryCubes = await this.getCubesFromQuery(query); + const filtersPerRole = {}; + queryCubes.forEach(cubeName => { + const cube = cubeEvaluator.cubeFromPath(cubeName); + if (this.rlsEnabledForCube(cube)) { + let hasRoleWithAccess = false; + for (const policy of this.getApplicablePolicies(cube, context, cubeEvaluator)) { + hasRoleWithAccess = true; + (policy?.rowLevel?.filters || []).forEach(filter => { + filtersPerRole[policy.role] = filtersPerRole[policy.role] || []; + filtersPerRole[policy.role].push(this.evaluateNestedFilter(filter, cube, context, cubeEvaluator)); + }); + } + if (!hasRoleWithAccess) { + query.segments.push({ + expression: () => '1 = 0', + cubeName: cube.name, + name: 'RLS Access Denied', + }); + } + } + }); + const rlsFilter = { + or: Object.keys(filtersPerRole).map(role => ({ + and: filtersPerRole[role] + })) + }; + query.filters.push(rlsFilter); + return query; + } + async compilerCacheFn(requestId, key, path) { const compilers = await this.getCompilers({ requestId }); if (this.sqlCache) { @@ -229,22 +334,94 @@ export class CompilerApi { ); } - async metaConfig(options = {}) { + filterVisibilityByAccessPolicy(cubeEvaluator, context, cubes) { + const isMemberVisibleInContext = {}; + + if (!this.isRlsEnabled(cubeEvaluator)) { + return cubes; + } + + for (const cube of cubes.filter(c => this.rlsEnabledForCube(c.config))) { + console.log('filterVisibilityByAccessPolicy', cube.config.name); + const evaluatedCube = cubeEvaluator.cubeFromPath(cube.config.name); + + const calculateContextVisibility = (item) => { + let isIncluded = false; + let isExplicitlyExcluded = false; + for (const policy of this.getApplicablePolicies(evaluatedCube, context, cubeEvaluator)) { + if (policy.memberLevel) { + isIncluded = policy.memberLevel.includesMembers.includes(item.name) || isIncluded; + isExplicitlyExcluded = policy.memberLevel.excludesMembers.includes(item.name) || isExplicitlyExcluded; + } else { + // TODO(maxim): validate this, it looks sus + // a policy without explicit memberLevel definition implicitly allow all members + isIncluded = true; + } + } + return isIncluded && !isExplicitlyExcluded; + }; + + for (const dimension of cube.config.dimensions) { + isMemberVisibleInContext[dimension.name] = calculateContextVisibility(dimension); + } + + for (const measure of cube.config.measures) { + isMemberVisibleInContext[measure.name] = calculateContextVisibility(measure); + } + + // TODO(maxim): should we filter segments as well? + // for (const segment of cube.config.segments) { + // isMemberVisibleInContext[segment.name] = calculateContextVisibility(segment); + // } + } + + console.log('isMemberVisibleInContext', isMemberVisibleInContext); + + const visibilityFilterForCube = (cube) => { + if (!this.rlsEnabledForCube(cube.config)) { + return (item) => item.isVisible; + } + return (item) => (item.isVisible && isMemberVisibleInContext[item.name] || false); + }; + + return cubes + .map((cube) => ({ + config: { + ...cube.config, + measures: cube.config.measures?.filter(visibilityFilterForCube(cube)), + dimensions: cube.config.dimensions?.filter(visibilityFilterForCube(cube)), + // segments: cube.config.segments?.filter(visibilityFilterForCube(cube)), + }, + })).filter( + cube => cube.config.measures?.length || + cube.config.dimensions?.length || + cube.config.segments?.length + ); + } + + async metaConfig(requestContext, options = {}) { const { includeCompilerId, ...restOptions } = options; const compilers = await this.getCompilers(restOptions); + const { cubes } = compilers.metaTransformer; + const filteredCubes = this.filterVisibilityByAccessPolicy(compilers.cubeEvaluator, requestContext, cubes); if (includeCompilerId) { return { - cubes: compilers.metaTransformer.cubes, + cubes: filteredCubes, compilerId: compilers.compilerId, }; } - return compilers.metaTransformer.cubes; + return filteredCubes; } - async metaConfigExtended(options) { - const { metaTransformer } = await this.getCompilers(options); + async metaConfigExtended(requestContext, options) { + const { metaTransformer, cubeEvaluator } = await this.getCompilers(options); + const filteredCubes = this.filterVisibilityByAccessPolicy( + cubeEvaluator, + requestContext, + metaTransformer?.cubes + ); return { - metaConfig: metaTransformer?.cubes, + metaConfig: filteredCubes, cubeDefinitions: metaTransformer?.cubeEvaluator?.cubeDefinitions, }; } diff --git a/packages/cubejs-server-core/test/unit/index.test.ts b/packages/cubejs-server-core/test/unit/index.test.ts index 5531e21e544d1..a5451db723a02 100644 --- a/packages/cubejs-server-core/test/unit/index.test.ts +++ b/packages/cubejs-server-core/test/unit/index.test.ts @@ -349,7 +349,7 @@ describe('index.test', () => { const metaConfigExtendedSpy = jest.spyOn(compilerApi, 'metaConfigExtended'); test('CompilerApi metaConfig', async () => { - const metaConfig = await compilerApi.metaConfig({ requestId: 'XXX' }); + const metaConfig = await compilerApi.metaConfig({ securityContext: {} }, { requestId: 'XXX' }); expect((metaConfig)?.length).toBeGreaterThan(0); expect(metaConfig[0]).toHaveProperty('config'); expect(metaConfig[0].config.hasOwnProperty('sql')).toBe(false); @@ -358,7 +358,7 @@ describe('index.test', () => { }); test('CompilerApi metaConfigExtended', async () => { - const metaConfigExtended = await compilerApi.metaConfigExtended({ requestId: 'XXX' }); + const metaConfigExtended = await compilerApi.metaConfigExtended({ securityContext: {} }, { requestId: 'XXX' }); expect(metaConfigExtended).toHaveProperty('metaConfig'); expect(metaConfigExtended.metaConfig.length).toBeGreaterThan(0); expect(metaConfigExtended).toHaveProperty('cubeDefinitions'); @@ -378,14 +378,14 @@ describe('index.test', () => { const metaConfigExtendedSpy = jest.spyOn(compilerApi, 'metaConfigExtended'); test('CompilerApi metaConfig', async () => { - const metaConfig = await compilerApi.metaConfig({ requestId: 'XXX' }); + const metaConfig = await compilerApi.metaConfig({ securityContext: {} }, { requestId: 'XXX' }); expect(metaConfig).toEqual([]); expect(metaConfigSpy).toHaveBeenCalled(); metaConfigSpy.mockClear(); }); test('CompilerApi metaConfigExtended', async () => { - const metaConfigExtended = await compilerApi.metaConfigExtended({ requestId: 'XXX' }); + const metaConfigExtended = await compilerApi.metaConfigExtended({ securityContext: {} }, { requestId: 'XXX' }); expect(metaConfigExtended).toHaveProperty('metaConfig'); expect(metaConfigExtended.metaConfig).toEqual([]); expect(metaConfigExtended).toHaveProperty('cubeDefinitions');