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..e96465c7c0f16 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 @@ -1180,6 +1180,7 @@ class ApiGateway { }, context); const startTime = new Date().getTime(); + const compilerApi = await this.getCompilerApi(context); let normalizedQueries: NormalizedQuery[] = await Promise.all( queries.map( @@ -1195,8 +1196,14 @@ class ApiGateway { } const normalizedQuery = normalizeQuery(currentQuery, persistent); - let rewrittenQuery = await this.queryRewrite( + // First apply cube/view level security policies + const queryWithRlsFilters = await compilerApi.applyRowLevelSecurity( normalizedQuery, + context + ); + // Then apply user-supplied queryRewrite + let rewrittenQuery = await this.queryRewrite( + queryWithRlsFilters, context, ); @@ -1693,7 +1700,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 +1810,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-api-gateway/src/helpers/prepareAnnotation.ts b/packages/cubejs-api-gateway/src/helpers/prepareAnnotation.ts index dc07336589072..268e354279e0e 100644 --- a/packages/cubejs-api-gateway/src/helpers/prepareAnnotation.ts +++ b/packages/cubejs-api-gateway/src/helpers/prepareAnnotation.ts @@ -48,7 +48,8 @@ const annotation = ( ) => (member: string | MemberExpression): undefined | [string, ConfigItem] => { const [cubeName, fieldName] = (member).expression ? [(member).cubeName, (member).name] : (member).split('.'); const memberWithoutGranularity = [cubeName, fieldName].join('.'); - const config: ConfigItem = configMap[cubeName][memberType] + const cubeConfig = configMap[cubeName]; + const config: ConfigItem = cubeConfig && cubeConfig[memberType] .find(m => m.name === memberWithoutGranularity); if (!config) { diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index 99f7236e5a4a9..658774e98919c 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -471,7 +471,7 @@ describe('API Gateway', () => { queryRewrite: async (query, _context) => { query.limit = 2; return query; - } + }, } ); diff --git a/packages/cubejs-api-gateway/test/mocks.ts b/packages/cubejs-api-gateway/test/mocks.ts index a43db82711ea7..18def17db3a7c 100644 --- a/packages/cubejs-api-gateway/test/mocks.ts +++ b/packages/cubejs-api-gateway/test/mocks.ts @@ -75,6 +75,10 @@ export const compilerApi = jest.fn().mockImplementation(async () => ({ return 'postgres'; }, + async applyRowLevelSecurity(query: any) { + return query; + }, + async metaConfig() { return [ { diff --git a/packages/cubejs-schema-compiler/src/compiler/CompilerCache.ts b/packages/cubejs-schema-compiler/src/compiler/CompilerCache.ts index ef10a96df615e..3ec5b0b2c25ab 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CompilerCache.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CompilerCache.ts @@ -4,6 +4,8 @@ import { QueryCache } from '../adapter/QueryCache'; export class CompilerCache extends QueryCache { protected readonly queryCache: LRUCache; + protected readonly rbacCache: LRUCache; + public constructor({ maxQueryCacheSize, maxQueryCacheAge }) { super(); @@ -12,6 +14,15 @@ export class CompilerCache extends QueryCache { maxAge: (maxQueryCacheAge * 1000) || 1000 * 60 * 10, updateAgeOnGet: true }); + + this.rbacCache = new LRUCache({ + max: 10000, + maxAge: 1000 * 60 * 5, // 5 minutes + }); + } + + public getRbacCacheInstance(): LRUCache { + return this.rbacCache; } public getQueryCache(key: unknown): QueryCache { diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts index a88937888eb70..ca2eefe4ca7df 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts @@ -65,6 +65,8 @@ export class CubeEvaluator extends CubeSymbols { public byFileName: Record = {}; + private isRbacEnabledCache: boolean | null = null; + public constructor( protected readonly cubeValidator: CubeValidator ) { @@ -112,9 +114,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', 'segments']; + 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 => ({ @@ -515,6 +579,19 @@ export class CubeEvaluator extends CubeSymbols { return path.split('.'); } + public isRbacEnabledForCube(cube: any): boolean { + return cube.accessPolicy && cube.accessPolicy.length; + } + + public isRbacEnabled(): boolean { + if (this.isRbacEnabledCache === null) { + this.isRbacEnabledCache = this.cubeNames().some( + cubeName => this.isRbacEnabledForCube(this.cubeFromPath(cubeName)) + ); + } + return this.isRbacEnabledCache; + } + protected parsePathAnyType(path) { // Should throw UserError in case of parse error this.byPathAnyType(path); diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js index 09b689697d0f5..ee8b0ab6e333f 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js @@ -10,6 +10,10 @@ 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 has been deprecated, however security_context (lowecase) + // is allowed in RBAC policies for query-time attribute matching + security_context: 'securityContext', + securityContext: 'securityContext', FILTER_PARAMS: 'filterParams', FILTER_GROUP: 'filterGroup', SQL_UTILS: 'sqlUtils' @@ -139,6 +143,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 +411,34 @@ export class CubeSymbols { }); } + /** + * This method is mainly used for evaluating RLS conditions and filters. + * It allows referencing security_context (lowecase) in dynamic conditions or filter values. + * + * It currently does not support async calls because inner resolveSymbol and + * resolveSymbolsCall are sync. Async support may be added later with deeper + * refactoring. + */ + 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; diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index a49d0a98181ef..1dd4bc256f82a 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -25,7 +25,7 @@ export const nonStringFields = new Set([ 'external', 'useOriginalSqlPreAggregations', 'readOnly', - 'prefix' + 'prefix', ]); const identifierRegex = /^[_a-zA-Z][_a-zA-Z0-9]*$/; @@ -615,6 +615,64 @@ 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({ + includes: Joi.alternatives([ + Joi.string().valid('*'), + Joi.array().items(Joi.string()) + ]), + excludes: Joi.alternatives([ + Joi.string().valid('*'), + 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), + allowAll: Joi.boolean().valid(true).strict(), +}).xor('filters', 'allowAll'); + +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.required()), }; const cubeSchema = inherit(baseSchema, { @@ -726,6 +785,7 @@ const viewSchema = inherit(baseSchema, { 'object.oxor': 'Using split together with prefix is not supported' }) ), + accessPolicy: Joi.array().items(RolePolicySchema.required()), }); function formatErrorMessageFromDetails(explain, d) { diff --git a/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts b/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts index 2cbf2d049e31b..d75087ddabbdf 100644 --- a/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts @@ -146,7 +146,20 @@ export class YamlCompiler { return this.parsePythonIntoArrowFunction(obj, cubeName, obj, errorsReport); } else if (Array.isArray(obj)) { const resultAst = t.program([t.expressionStatement(t.arrayExpression(obj.map(code => { - const ast = this.parsePythonAndTranspileToJs(code, errorsReport); + let ast: t.Program | t.NullLiteral | t.BooleanLiteral | t.NumericLiteral | null = null; + // Special case for accessPolicy.rowLevel.filter.values and other values-like fields + if (propertyPath[propertyPath.length - 1] === 'values') { + if (typeof code === 'string') { + ast = this.parsePythonAndTranspileToJs(`f"${this.escapeDoubleQuotes(code)}"`, errorsReport); + } else if (typeof code === 'boolean') { + ast = t.booleanLiteral(code); + } else if (typeof code === 'number') { + ast = t.numericLiteral(code); + } + } + if (ast === null) { + ast = this.parsePythonAndTranspileToJs(code, errorsReport); + } return this.extractProgramBodyIfNeeded(ast); }).filter(ast => !!ast)))]); return this.astIntoArrowFunction(resultAst, '', cubeName); diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index f55c71dca29c2..a7dcb2e85b26a 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -25,6 +25,9 @@ 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$/, ]; 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..aab99d32ccf97 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,12 @@ describe('Schema Testing', () => { CubeD: { relationship: 'belongsTo' } }); }); + + it('valid schema with accessPolicy', async () => { + const { compiler } = prepareCompiler([ + createCubeSchemaWithAccessPolicy('ProtectedCube'), + ]); + await compiler.compile(); + compiler.throwIfAnyErrors(); + }); }); diff --git a/packages/cubejs-schema-compiler/test/unit/utils.ts b/packages/cubejs-schema-compiler/test/unit/utils.ts index 969bf7f8cc280..4ad63e5538b3a 100644 --- a/packages/cubejs-schema-compiler/test/unit/utils.ts +++ b/packages/cubejs-schema-compiler/test/unit/utils.ts @@ -96,6 +96,134 @@ 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: "*", + rowLevel: { + allowAll: true + } + }, + { + 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..2b926a0ec6e24 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,55 @@ 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"] + - or: + - member: "{CUBE}.created_at" + operator: notInDateRange + values: + - 2022-01-01 + - "{ security_context.currentDate }" + - member: "created_at" + operator: equals + values: + - "{ securityContext.currentDate }" + memberLevel: + includes: + - status + - role: manager + memberLevel: + excludes: + - 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..20abab13a033e 100644 --- a/packages/cubejs-server-core/src/core/CompilerApi.js +++ b/packages/cubejs-server-core/src/core/CompilerApi.js @@ -11,6 +11,7 @@ export class CompilerApi { * @param {DbTypeAsyncFn} dbType * @param {*} options */ + constructor(repository, dbType, options) { this.repository = repository; this.dbType = dbType; @@ -22,6 +23,7 @@ export class CompilerApi { this.allowUngroupedWithoutPrimaryKey = this.options.allowUngroupedWithoutPrimaryKey; this.convertTzForRawTimeDimension = this.options.convertTzForRawTimeDimension; this.schemaVersion = this.options.schemaVersion; + this.contextToRoles = this.options.contextToRoles; this.compileContext = options.compileContext; this.allowJsDuplicatePropsInSchema = options.allowJsDuplicatePropsInSchema; this.sqlCache = options.sqlCache; @@ -185,6 +187,195 @@ export class CompilerApi { } } + async getRolesFromContext(context) { + if (!this.contextToRoles) { + return new Set(); + } + return new Set(await this.contextToRoles(context)); + } + + 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])); + } + + hashRequestContext(context) { + if (!context.__hash) { + context.__hash = crypto.createHash('md5').update(JSON.stringify(context)).digest('hex'); + } + return context.__hash; + } + + async getApplicablePolicies(cube, context, compilers) { + const cache = compilers.compilerCache.getRbacCacheInstance(); + const cacheKey = `${cube.name}_${this.hashRequestContext(context)}`; + if (!cache.has(cacheKey)) { + const userRoles = await this.getRolesFromContext(context); + const policies = cube.accessPolicy.filter(policy => { + const evaluatedConditions = (policy.conditions || []).map( + condition => compilers.cubeEvaluator.evaluateContextFunction(cube, condition.if, context) + ); + const res = this.userHasRole(userRoles, policy.role) && this.roleMeetsConditions(evaluatedConditions); + return res; + }); + cache.set(cacheKey, policies); + } + return cache.get(cacheKey); + } + + 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; + } + + /** + * This method rewrites the query according to RBAC row level security policies. + * + * If RBAC is enabled, it looks at all the Cubes from the query with accessPolicy defined. + * It extracts all policies applicable to for the current user context (contextToRoles() + conditions). + * It then generates an rls filter by + * - combining all filters for the same role with AND + * - combining all filters for different roles with OR + * - combining cube and view filters with AND + */ + async applyRowLevelSecurity(query, context) { + const compilers = await this.getCompilers({ requestId: query.requestId }); + const { cubeEvaluator } = compilers; + + if (!cubeEvaluator.isRbacEnabled()) { + return query; + } + + const queryCubes = await this.getCubesFromQuery(query); + + // We collect Cube and View filters separately because they have to be + // applied in "two layers": first Cube filters, then View filters on top + const cubeFiltersPerCubePerRole = {}; + const viewFiltersPerCubePerRole = {}; + const hasAllowAllForCube = {}; + + for (const cubeName of queryCubes) { + const cube = cubeEvaluator.cubeFromPath(cubeName); + const filtersMap = cube.isView ? viewFiltersPerCubePerRole : cubeFiltersPerCubePerRole; + + if (cubeEvaluator.isRbacEnabledForCube(cube)) { + let hasRoleWithAccess = false; + const userPolicies = await this.getApplicablePolicies(cube, context, compilers); + + for (const policy of userPolicies) { + hasRoleWithAccess = true; + (policy?.rowLevel?.filters || []).forEach(filter => { + filtersMap[cubeName] = filtersMap[cubeName] || {}; + filtersMap[cubeName][policy.role] = filtersMap[cubeName][policy.role] || []; + filtersMap[cubeName][policy.role].push( + this.evaluateNestedFilter(filter, cube, context, cubeEvaluator) + ); + }); + if (!policy?.rowLevel || policy?.rowLevel?.allowAll) { + hasAllowAllForCube[cubeName] = true; + // We don't have a way to add an "all alloed" filter like `WHERE 1 = 1` or something. + // Instead, we'll just mark that the user has "all" access to a given cube and remove + // all filters later + break; + } + } + + if (!hasRoleWithAccess) { + // This is a hack that will make sure that the query returns no result + query.segments = query.segments || []; + query.segments.push({ + expression: () => '1 = 0', + cubeName: cube.name, + name: 'rlsAccessDenied', + }); + // If we hit this condition there's no need to evaluate the rest of the policy + break; + } + } + } + + const rlsFilter = this.buildFinalRlsFilter( + cubeFiltersPerCubePerRole, + viewFiltersPerCubePerRole, + hasAllowAllForCube + ); + query.filters = query.filters || []; + query.filters.push(rlsFilter); + return query; + } + + buildFinalRlsFilter(cubeFiltersPerCubePerRole, viewFiltersPerCubePerRole, hasAllowAllForCube) { + // - delete all filters for cubes where the user has allowAll + // - combine the rest into per role maps + // - join all filters for the same role with AND + // - join all filters for different roles with OR + // - join cube and view filters with AND + + const roleReducer = (filtersMap) => (acc, cubeName) => { + if (!hasAllowAllForCube[cubeName]) { + Object.keys(filtersMap[cubeName]).forEach(role => { + acc[role] = (acc[role] || []).concat(filtersMap[cubeName][role]); + }); + } + return acc; + }; + + const cubeFiltersPerRole = Object.keys(cubeFiltersPerCubePerRole).reduce( + roleReducer(cubeFiltersPerCubePerRole), + {} + ); + const viewFiltersPerRole = Object.keys(viewFiltersPerCubePerRole).reduce( + roleReducer(viewFiltersPerCubePerRole), + {} + ); + + return { + and: [{ + or: Object.keys(cubeFiltersPerRole).map(role => ({ + and: cubeFiltersPerRole[role] + })) + }, { + or: Object.keys(viewFiltersPerRole).map(role => ({ + and: viewFiltersPerRole[role] + })) + }] + }; + } + async compilerCacheFn(requestId, key, path) { const compilers = await this.getCompilers({ requestId }); if (this.sqlCache) { @@ -229,23 +420,104 @@ export class CompilerApi { ); } - async metaConfig(options = {}) { + /** + * if RBAC is enabled, this method is used to filter out the cubes that the + * user doesn't have access from meta responses. + * It evaluates all applicable memeberLevel accessPolicies givean a context + * and retains members that are allowed by any policy (most permissive set). + */ + async filterVisibilityByAccessPolicy(compilers, context, cubes) { + const isMemberVisibleInContext = {}; + const { cubeEvaluator } = compilers; + + if (!cubeEvaluator.isRbacEnabled()) { + return cubes; + } + + for (const cube of cubes) { + const evaluatedCube = cubeEvaluator.cubeFromPath(cube.config.name); + if (cubeEvaluator.isRbacEnabledForCube(evaluatedCube)) { + const applicablePolicies = await this.getApplicablePolicies(evaluatedCube, context, compilers); + + const computeMemberVisibility = (item) => { + let isIncluded = false; + let isExplicitlyExcluded = false; + for (const policy of applicablePolicies) { + if (policy.memberLevel) { + isIncluded = policy.memberLevel.includesMembers.includes(item.name) || isIncluded; + isExplicitlyExcluded = policy.memberLevel.excludesMembers.includes(item.name) || isExplicitlyExcluded; + } else { + isIncluded = true; + } + } + return isIncluded && !isExplicitlyExcluded; + }; + + for (const dimension of cube.config.dimensions) { + isMemberVisibleInContext[dimension.name] = computeMemberVisibility(dimension); + } + + for (const measure of cube.config.measures) { + isMemberVisibleInContext[measure.name] = computeMemberVisibility(measure); + } + + for (const segment of cube.config.segments) { + isMemberVisibleInContext[segment.name] = computeMemberVisibility(segment); + } + } + } + + const visibilityFilterForCube = (cube) => { + const evaluatedCube = cubeEvaluator.cubeFromPath(cube.config.name); + if (!cubeEvaluator.isRbacEnabledForCube(evaluatedCube)) { + 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 = await this.filterVisibilityByAccessPolicy( + compilers, + 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 compilers = await this.getCompilers(options); + const filteredCubes = await this.filterVisibilityByAccessPolicy( + compilers, + requestContext, + compilers.metaTransformer?.cubes + ); return { - metaConfig: metaTransformer?.cubes, - cubeDefinitions: metaTransformer?.cubeEvaluator?.cubeDefinitions, + metaConfig: filteredCubes, + cubeDefinitions: compilers.metaTransformer?.cubeEvaluator?.cubeDefinitions, }; } diff --git a/packages/cubejs-server-core/src/core/optionsValidate.ts b/packages/cubejs-server-core/src/core/optionsValidate.ts index 576271cdbc4fd..157d2acb84cd9 100644 --- a/packages/cubejs-server-core/src/core/optionsValidate.ts +++ b/packages/cubejs-server-core/src/core/optionsValidate.ts @@ -72,6 +72,7 @@ const schemaOptions = Joi.object().keys({ // cacheAndQueueDriver: Joi.string().valid('cubestore', 'memory'), contextToAppId: Joi.func(), + contextToRoles: Joi.func(), contextToOrchestratorId: Joi.func(), contextToDataSourceId: Joi.func(), contextToApiScopes: Joi.func(), diff --git a/packages/cubejs-server-core/src/core/server.ts b/packages/cubejs-server-core/src/core/server.ts index 72e6d9130bd9c..673096142a720 100644 --- a/packages/cubejs-server-core/src/core/server.ts +++ b/packages/cubejs-server-core/src/core/server.ts @@ -506,6 +506,7 @@ export class CubejsServerCore { ), externalDialectClass: this.options.externalDialectFactory && this.options.externalDialectFactory(context), schemaVersion: currentSchemaVersion, + contextToRoles: this.options.contextToRoles, preAggregationsSchema: await this.preAggregationsSchema(context), context, allowJsDuplicatePropsInSchema: this.options.allowJsDuplicatePropsInSchema, @@ -667,6 +668,7 @@ export class CubejsServerCore { options.dbType || this.options.dbType, { schemaVersion: options.schemaVersion || this.options.schemaVersion, + contextToRoles: this.options.contextToRoles, devServer: this.options.devServer, logger: this.logger, externalDbType: options.externalDbType, diff --git a/packages/cubejs-server-core/src/core/types.ts b/packages/cubejs-server-core/src/core/types.ts index 763f86b6051cb..b5280b69b71f7 100644 --- a/packages/cubejs-server-core/src/core/types.ts +++ b/packages/cubejs-server-core/src/core/types.ts @@ -120,6 +120,7 @@ export type DatabaseType = | 'materialize'; export type ContextToAppIdFn = (context: RequestContext) => string | Promise; +export type ContextToRolesFn = (context: RequestContext) => string[] | Promise; export type ContextToOrchestratorIdFn = (context: RequestContext) => string | Promise; export type OrchestratorOptionsFn = (context: RequestContext) => OrchestratorOptions | Promise; @@ -176,6 +177,7 @@ export interface CreateOptions { externalDialectFactory?: ExternalDialectFactoryFn; cacheAndQueueDriver?: CacheAndQueryDriverType; contextToAppId?: ContextToAppIdFn; + contextToRoles?: ContextToRolesFn; contextToOrchestratorId?: ContextToOrchestratorIdFn; contextToApiScopes?: ContextToApiScopesFn; repositoryFactory?: (context: RequestContext) => SchemaFileRepository; 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'); diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js b/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js new file mode 100644 index 0000000000000..45dd3af535f9c --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js @@ -0,0 +1,27 @@ +module.exports = { + contextToRoles: async (context) => context.securityContext.auth?.roles || [], + checkSqlAuth: async (req, user, password) => { + if (user === 'admin') { + if (password && password !== 'admin_password') { + throw new Error(`Password doesn't match for ${user}`); + } + return { + password, + superuser: true, + securityContext: { + auth: { + username: 'admin', + userAttributes: { + region: 'CA', + city: 'Fresno', + canHaveAdmin: true, + minDefaultId: 10000, + }, + roles: ['admin', 'ownder', 'hr'], + }, + }, + }; + } + throw new Error(`User "${user}" doesn't exist`); + } +}; diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/line_items.js b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/line_items.js new file mode 100644 index 0000000000000..3d4ddd8ced1a5 --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/line_items.js @@ -0,0 +1,76 @@ +cube('line_items', { + sql_table: 'public.line_items', + + data_source: 'default', + + joins: { + orders: { + relationship: 'many_to_one', + sql: `${orders}.id = ${line_items}.order_id`, + }, + + }, + + dimensions: { + id: { + sql: 'id', + type: 'number', + primary_key: true, + }, + + created_at: { + sql: 'created_at', + type: 'time', + }, + + price_dim: { + sql: 'price', + type: 'number', + }, + }, + + measures: { + count: { + type: 'count', + }, + + price: { + sql: 'price', + type: 'sum', + }, + + quantity: { + sql: 'quantity', + type: 'sum', + }, + }, + + accessPolicy: [ + { + role: '*', + rowLevel: { + filters: [{ + member: 'id', + operator: 'gt', + // This is to test dynamic values based on security context + values: [`${security_context.auth?.userAttributes?.minDefaultId || 20000}`], + }] + } + }, + { + role: 'admin', + conditions: [ + { + if: security_context.auth?.userAttributes?.region === 'CA', + }, + ], + rowLevel: { + // The "allowAll" flag should negate the default `id` filter + allowAll: true, + }, + memberLevel: { + excludes: ['created_at'], + }, + }, + ], +}); diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/orders.js b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/orders.js new file mode 100644 index 0000000000000..04b4ec49f7192 --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/orders.js @@ -0,0 +1,76 @@ +cube('orders', { + sql_table: 'public.orders', + + data_source: 'default', + + joins: { + line_items: { + relationship: 'one_to_many', + sql: `${orders}.id = ${line_items}.order_id`, + }, + }, + + dimensions: { + id: { + sql: 'id', + type: 'number', + primary_key: true, + }, + + created_at: { + sql: 'created_at', + type: 'time', + }, + }, + + measures: { + count: { + type: 'count', + }, + }, + + accessPolicy: [ + { + role: '*', + memberLevel: { + // This cube is "private" by default and only accessible via views + includes: [], + }, + rowLevel: { + filters: [ + { + member: 'id', + operator: 'equals', + values: [1], + }, + ], + }, + }, + { + role: 'admin', + memberLevel: { + // This cube is "private" by default and only accessible via views + includes: [], + }, + rowLevel: { + filters: [ + { + or: [ + { + member: `${CUBE}.id`, + operator: 'equals', + values: [10], + }, + { + // Testing different ways of referencing cube members + member: 'id', + operator: 'equals', + values: ['11'], + }, + ], + }, + ], + }, + }, + ], +}); diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/orders_open.yaml b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/orders_open.yaml new file mode 100644 index 0000000000000..58f5b5aa1379b --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/orders_open.yaml @@ -0,0 +1,19 @@ +cubes: + # An open cube with no access policy + - name: orders_open + sql_table: orders + + dimensions: + - name: id + sql: id + type: string + primary_key: true + + - name: created_at + sql: created_at + type: time + + measures: + - name: count + sql: id + type: count diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/users.yaml b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/users.yaml new file mode 100644 index 0000000000000..d2b113e8a39ea --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/users.yaml @@ -0,0 +1,49 @@ +cubes: + - name: users + sql_table: users + + measures: + - name: count + sql: id + type: count + + dimensions: + - name: city + sql: city + type: string + + - name: id + sql: id + type: number + primary_key: true + + access_policy: + - role: "*" + - role: admin + conditions: + # This thing will fail if there's no auth info in the context + # Unfortunately, as of now, there's no way to write more complex expressions + # that would allow us to check for the existence of the auth object + - if: "{ security_context.auth.userAttributes.canHaveAdmin }" + row_level: + filters: + - or: + - and: + - member: "{CUBE}.city" + operator: notStartsWith + values: + - London + - "{ security_context.auth.userAttributes.city }" + # mixing string, dynamic values, integers and bools should not + # cause any compilation issues + - 4 + - true + - member: "city" + operator: notEquals + values: + - 'San Francisco' + - member: "{CUBE}.city" + operator: equals + values: + - "New York" + diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/model/views/views.yaml b/packages/cubejs-testing/birdbox-fixtures/rbac/model/views/views.yaml new file mode 100644 index 0000000000000..05df126f7feae --- /dev/null +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/views/views.yaml @@ -0,0 +1,37 @@ +views: + - name: line_items_view_price_gt_200 + cubes: + - join_path: line_items + includes: "*" + access_policy: + - role: "*" + row_level: + filters: + - member: "${CUBE}.price_dim" + operator: gt + values: + - 200 + + - name: line_items_view_joined_orders + cubes: + - join_path: line_items + includes: "*" + - join_path: line_items.orders + prefix: true + includes: "*" + + - name: line_items_view_no_policy + cubes: + - join_path: line_items + includes: "*" + + - name: orders_view + cubes: + - join_path: orders + includes: "*" + access_policy: + - role: admin + member_level: + includes: "*" + row_level: + allow_all: true diff --git a/packages/cubejs-testing/package.json b/packages/cubejs-testing/package.json index b3554b510c752..61cc49e436f2c 100644 --- a/packages/cubejs-testing/package.json +++ b/packages/cubejs-testing/package.json @@ -73,6 +73,7 @@ "smoke:postgres": "jest --verbose -i dist/test/smoke-postgres.test.js", "smoke:redshift": "jest --verbose -i dist/test/smoke-redshift.test.js", "smoke:redshift:snapshot": "jest --verbose --updateSnapshot -i dist/test/smoke-redshift.test.js", + "smoke:rbac": "TZ=UTC jest --verbose --forceExit -i dist/test/smoke-rbac.test.js", "smoke:cubesql": "TZ=UTC jest --verbose --forceExit -i dist/test/smoke-cubesql.test.js", "smoke:cubesql:snapshot": "TZ=UTC jest --verbose --forceExit --updateSnapshot -i dist/test/smoke-cubesql.test.js", "smoke:prestodb": "jest --verbose -i dist/test/smoke-prestodb.test.js", diff --git a/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap b/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap new file mode 100644 index 0000000000000..10275b929d670 --- /dev/null +++ b/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap @@ -0,0 +1,1083 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from line_items: line_items 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 263, + "price_dim": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 180, + "price_dim": 180, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 169, + "price_dim": 169, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 156, + "price_dim": 156, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 36, + "price_dim": 36, + "quantity": 5, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 245, + "price_dim": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 232, + "price_dim": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 63, + "price_dim": 63, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 68, + "price_dim": 68, + "quantity": 6, + }, +] +`; + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from line_items_view_joined_orders: orders_view 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + "orders_count": "1", + "orders_created_at": 2018-10-23T07:54:39.000Z, + "orders_id": 1, + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "id": 10, + "orders_count": "1", + "orders_created_at": 2019-12-16T08:09:36.000Z, + "orders_id": 10, + "price": 68, + "price_dim": 68, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-22T15:52:17.000Z, + "id": 11, + "orders_count": "1", + "orders_created_at": 2019-04-22T15:52:17.000Z, + "orders_id": 11, + "price": 170, + "price_dim": 170, + "quantity": 1, + }, +] +`; + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from line_items_view_no_policy: line_items_view_no_policy 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + "id": 2, + "price": 263, + "price_dim": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-05-13T21:23:08.000Z, + "id": 3, + "price": 180, + "price_dim": 180, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-04-10T22:51:15.000Z, + "id": 4, + "price": 169, + "price_dim": 169, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-16T15:00:34.000Z, + "id": 5, + "price": 156, + "price_dim": 156, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-05-23T04:25:27.000Z, + "id": 6, + "price": 36, + "price_dim": 36, + "quantity": 5, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + "id": 7, + "price": 245, + "price_dim": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + "id": 8, + "price": 232, + "price_dim": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-11-15T18:22:17.000Z, + "id": 9, + "price": 63, + "price_dim": 63, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "id": 10, + "price": 68, + "price_dim": 68, + "quantity": 6, + }, +] +`; + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from line_items_view_price_gt_200: line_items_view_price_gt_200 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + "id": 2, + "price": 263, + "price_dim": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + "id": 7, + "price": 245, + "price_dim": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + "id": 8, + "price": 232, + "price_dim": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-12-04T06:39:17.000Z, + "id": 12, + "price": 266, + "price_dim": 266, + "quantity": 9, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-01-28T20:03:53.000Z, + "id": 13, + "price": 286, + "price_dim": 286, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-08-04T21:41:23.000Z, + "id": 15, + "price": 237, + "price_dim": 237, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-14T05:31:34.000Z, + "id": 25, + "price": 262, + "price_dim": 262, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2016-01-01T23:00:21.000Z, + "id": 29, + "price": 241, + "price_dim": 241, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-05T18:07:39.000Z, + "id": 34, + "price": 249, + "price_dim": 249, + "quantity": 8, + }, +] +`; + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from orders: orders_open 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-05-13T21:23:08.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-04-10T22:51:15.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-16T15:00:34.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-11-15T18:22:17.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-22T15:52:17.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-12-04T06:39:17.000Z, + }, +] +`; + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from orders_view: orders_view 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "id": 10, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-22T15:52:17.000Z, + "id": 11, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + }, +] +`; + +exports[`Cube RBAC Engine RBAC through SQL API SELECT * from users: users 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, +] +`; + +exports[`Cube RBAC Engine RBAC via REST API line_items hidden created_at: line_items_view_no_policy_rest 1`] = ` +Array [ + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-01-01T23:00:21.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-02-27T06:09:59.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-02-27T11:38:04.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-03-06T17:41:43.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-03-07T19:41:43.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-03-13T22:34:22.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-03-21T21:13:47.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-05-01T07:26:55.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-05-13T07:03:15.000", + }, + Object { + "line_items_view_no_policy.count": "1", + "line_items_view_no_policy.created_at": "2016-05-15T22:44:26.000", + }, +] +`; + +exports[`Cube RBAC Engine RBAC via REST API orders_view and cube with default policy: orders_open_rest 1`] = ` +Array [ + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-01T16:45:36.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-01T17:23:48.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-01T17:41:57.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-01T23:00:21.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-02T14:06:50.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-02T21:56:15.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-02T22:47:22.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-04T09:34:08.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-04T22:18:52.000", + }, + Object { + "orders_open.count": "1", + "orders_open.created_at": "2016-01-05T01:39:46.000", + }, +] +`; + +exports[`Cube RBAC Engine RBAC via REST API orders_view and cube with default policy: orders_view_rest 1`] = `Array []`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from line_items: line_items 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 263, + "price_dim": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 180, + "price_dim": 180, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 169, + "price_dim": 169, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 156, + "price_dim": 156, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 36, + "price_dim": 36, + "quantity": 5, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 245, + "price_dim": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 232, + "price_dim": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 63, + "price_dim": 63, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "price": 68, + "price_dim": 68, + "quantity": 6, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from line_items_view_joined_orders: orders_view 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + "orders_count": "1", + "orders_created_at": 2018-10-23T07:54:39.000Z, + "orders_id": 1, + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "id": 10, + "orders_count": "1", + "orders_created_at": 2019-12-16T08:09:36.000Z, + "orders_id": 10, + "price": 68, + "price_dim": 68, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-22T15:52:17.000Z, + "id": 11, + "orders_count": "1", + "orders_created_at": 2019-04-22T15:52:17.000Z, + "orders_id": 11, + "price": 170, + "price_dim": 170, + "quantity": 1, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from line_items_view_no_policy: line_items_view_no_policy 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + "id": 2, + "price": 263, + "price_dim": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-05-13T21:23:08.000Z, + "id": 3, + "price": 180, + "price_dim": 180, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-04-10T22:51:15.000Z, + "id": 4, + "price": 169, + "price_dim": 169, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-16T15:00:34.000Z, + "id": 5, + "price": 156, + "price_dim": 156, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-05-23T04:25:27.000Z, + "id": 6, + "price": 36, + "price_dim": 36, + "quantity": 5, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + "id": 7, + "price": 245, + "price_dim": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + "id": 8, + "price": 232, + "price_dim": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-11-15T18:22:17.000Z, + "id": 9, + "price": 63, + "price_dim": 63, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "id": 10, + "price": 68, + "price_dim": 68, + "quantity": 6, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from line_items_view_price_gt_200: line_items_view_price_gt_200 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + "price": 267, + "price_dim": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + "id": 2, + "price": 263, + "price_dim": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + "id": 7, + "price": 245, + "price_dim": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + "id": 8, + "price": 232, + "price_dim": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-12-04T06:39:17.000Z, + "id": 12, + "price": 266, + "price_dim": 266, + "quantity": 9, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-01-28T20:03:53.000Z, + "id": 13, + "price": 286, + "price_dim": 286, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-08-04T21:41:23.000Z, + "id": 15, + "price": 237, + "price_dim": 237, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-14T05:31:34.000Z, + "id": 25, + "price": 262, + "price_dim": 262, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2016-01-01T23:00:21.000Z, + "id": 29, + "price": 241, + "price_dim": 241, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-05T18:07:39.000Z, + "id": 34, + "price": 249, + "price_dim": 249, + "quantity": 8, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from orders: orders_open 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-05-13T21:23:08.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-04-10T22:51:15.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-16T15:00:34.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-11-15T18:22:17.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-22T15:52:17.000Z, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-12-04T06:39:17.000Z, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from orders_view: orders_view 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "id": 10, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-22T15:52:17.000Z, + "id": 11, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "id": 1, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL API SELECT * from users: users 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, + Object { + "__cubeJoinField": null, + "__user": null, + "city": "Austin", + "count": "1", + }, +] +`; diff --git a/packages/cubejs-testing/test/smoke-cubesql.test.ts b/packages/cubejs-testing/test/smoke-cubesql.test.ts index a2ac9de69ab8f..9d0eec2177bda 100644 --- a/packages/cubejs-testing/test/smoke-cubesql.test.ts +++ b/packages/cubejs-testing/test/smoke-cubesql.test.ts @@ -33,7 +33,7 @@ describe('SQL API', () => { const conn = new PgClient({ database: 'db', port: pgPort, - host: 'localhost', + host: '127.0.0.1', user, password, ssl: false, diff --git a/packages/cubejs-testing/test/smoke-rbac.test.ts b/packages/cubejs-testing/test/smoke-rbac.test.ts new file mode 100644 index 0000000000000..b2476395968e7 --- /dev/null +++ b/packages/cubejs-testing/test/smoke-rbac.test.ts @@ -0,0 +1,249 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { afterAll, beforeAll, jest, expect } from '@jest/globals'; +import { sign } from 'jsonwebtoken'; +import { Client as PgClient } from 'pg'; +import cubejs, { CubeApi, Query } from '@cubejs-client/core'; +import { PostgresDBRunner } from '@cubejs-backend/testing-shared'; +import type { StartedTestContainer } from 'testcontainers'; + +import { BirdBox, getBirdbox } from '../src'; +import { + DEFAULT_CONFIG, + JEST_AFTER_ALL_DEFAULT_TIMEOUT, + JEST_BEFORE_ALL_DEFAULT_TIMEOUT, +} from './smoke-tests'; + +describe('Cube RBAC Engine', () => { + jest.setTimeout(60 * 5 * 1000); + + let birdbox: BirdBox; + let db: StartedTestContainer; + + const pgPort = 5656; + let connectionId = 0; + + async function createPostgresClient(user: string, password: string) { + connectionId++; + const currentConnId = connectionId; + + console.debug(`[pg] new connection ${currentConnId}`); + + const conn = new PgClient({ + database: 'db', + port: pgPort, + host: '127.0.0.1', + user, + password, + ssl: false, + }); + conn.on('error', (err) => { + console.log(err); + }); + conn.on('end', () => { + console.debug(`[pg] end ${currentConnId}`); + }); + + await conn.connect(); + + return conn; + } + + beforeAll(async () => { + db = await PostgresDBRunner.startContainer({}); + await PostgresDBRunner.loadEcom(db); + birdbox = await getBirdbox( + 'postgres', + { + ...DEFAULT_CONFIG, + // + CUBESQL_LOG_LEVEL: 'trace', + // + CUBEJS_DB_TYPE: 'postgres', + CUBEJS_DB_HOST: db.getHost(), + CUBEJS_DB_PORT: `${db.getMappedPort(5432)}`, + CUBEJS_DB_NAME: 'test', + CUBEJS_DB_USER: 'test', + CUBEJS_DB_PASS: 'test', + // + CUBEJS_PG_SQL_PORT: `${pgPort}`, + CUBESQL_SQL_PUSH_DOWN: 'true', + CUBESQL_STREAM_MODE: 'true', + }, + { + schemaDir: 'rbac/model', + cubejsConfig: 'rbac/cube.js', + } + ); + }, JEST_BEFORE_ALL_DEFAULT_TIMEOUT); + + afterAll(async () => { + await birdbox.stop(); + await db.stop(); + }, JEST_AFTER_ALL_DEFAULT_TIMEOUT); + + describe('RBAC via SQL API', () => { + let connection: PgClient; + + beforeAll(async () => { + connection = await createPostgresClient('admin', 'admin_password'); + }); + + afterAll(async () => { + await connection.end(); + }, JEST_AFTER_ALL_DEFAULT_TIMEOUT); + + test('SELECT * from line_items', async () => { + const res = await connection.query('SELECT * FROM line_items limit 10'); + // This query should return all rows because of the `allow_all` statement + // It should also exclude the `created_at` dimension as per memberLevel policy + expect(res.rows).toMatchSnapshot('line_items'); + }); + + // ??? + test('SELECT * from line_items_view_no_policy', async () => { + const res = await connection.query('SELECT * FROM line_items_view_no_policy limit 10'); + // This should query the line_items cube through the view that should + // allow for the ommitted `created_at` dimension to be included + expect(res.rows).toMatchSnapshot('line_items_view_no_policy'); + }); + + test('SELECT * from line_items_view_price_gt_200', async () => { + const res = await connection.query('SELECT * FROM line_items_view_price_gt_200 limit 10'); + // This query should add an extra filter by `price_dim` defined at the view level + expect(res.rows).toMatchSnapshot('line_items_view_price_gt_200'); + }); + + test('SELECT * from orders', async () => { + let failed = false; + try { + // Orders cube does not expose any members so, the query should fail + await connection.query('SELECT * FROM orders'); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + + const res = await connection.query('SELECT * FROM orders_open limit 10'); + // Open version of the orders cube should return everything + expect(res.rows).toMatchSnapshot('orders_open'); + }); + + test('SELECT * from orders_view', async () => { + const res = await connection.query('SELECT * FROM orders_view limit 10'); + // Orders cube should be visible via the view + expect(res.rows).toMatchSnapshot('orders_view'); + }); + + test('SELECT * from line_items_view_joined_orders', async () => { + const res = await connection.query('SELECT * FROM line_items_view_joined_orders limit 10'); + // Querying the line_items cube with joined orders should take into account + // orders row level policy and return only a few rows with select ids + expect(res.rows).toMatchSnapshot('orders_view'); + }); + + test('SELECT * from users', async () => { + const res = await connection.query('SELECT * FROM users limit 10'); + // Querying a cube with nested filters and mixed values should not cause any issues + expect(res.rows).toMatchSnapshot('users'); + }); + }); + + describe('RBAC via REST API', () => { + let client: CubeApi; + let defaultClient: CubeApi; + + const ADMIN_API_TOKEN = sign({ + auth: { + username: 'admin', + userAttributes: { + region: 'CA', + city: 'Fresno', + canHaveAdmin: true, + minDefaultId: 10000, + }, + roles: ['admin', 'ownder', 'hr'], + }, + }, DEFAULT_CONFIG.CUBEJS_API_SECRET, { + expiresIn: '2 days' + }); + + const DEFAULT_API_TOKEN = sign({ + auth: { + username: 'nobody', + userAttributes: {}, + roles: [], + }, + }, DEFAULT_CONFIG.CUBEJS_API_SECRET, { + expiresIn: '2 days' + }); + + beforeAll(async () => { + client = cubejs(async () => ADMIN_API_TOKEN, { + apiUrl: birdbox.configuration.apiUrl, + }); + defaultClient = cubejs(async () => DEFAULT_API_TOKEN, { + apiUrl: birdbox.configuration.apiUrl, + }); + }); + + test('line_items hidden created_at', async () => { + let query: Query = { + measures: ['line_items.count'], + dimensions: ['line_items.created_at'], + order: { + 'line_items.created_at': 'asc', + }, + }; + let error = ''; + try { + await client.load(query, {}); + } catch (e: any) { + error = e.toString(); + } + expect(error).toContain('You requested hidden member'); + query = { + measures: ['line_items_view_no_policy.count'], + dimensions: ['line_items_view_no_policy.created_at'], + order: { + 'line_items_view_no_policy.created_at': 'asc', + }, + limit: 10, + }; + const result = await client.load(query, {}); + expect(result.rawData()).toMatchSnapshot('line_items_view_no_policy_rest'); + }); + + test('orders_view and cube with default policy', async () => { + let error = ''; + try { + await defaultClient.load({ + measures: ['orders.count'], + }); + } catch (e: any) { + error = e.toString(); + } + expect(error).toContain('You requested hidden member'); + + let result = await defaultClient.load({ + measures: ['orders_view.count'], + dimensions: ['orders_view.created_at'], + order: { + 'orders_view.created_at': 'asc', + }, + }); + // It should only return one value allowed by the default policy + expect(result.rawData()).toMatchSnapshot('orders_view_rest'); + + result = await defaultClient.load({ + measures: ['orders_open.count'], + dimensions: ['orders_open.created_at'], + order: { + 'orders_open.created_at': 'asc', + }, + limit: 10 + }); + // order_open should return all values since it has no access policy + expect(result.rawData()).toMatchSnapshot('orders_open_rest'); + }); + }); +});