diff --git a/packages/cubejs-server-core/src/core/CompilerApi.js b/packages/cubejs-server-core/src/core/CompilerApi.js index 45ae80dc68375..1e7fd4e4bcb53 100644 --- a/packages/cubejs-server-core/src/core/CompilerApi.js +++ b/packages/cubejs-server-core/src/core/CompilerApi.js @@ -1,7 +1,7 @@ import crypto from 'crypto'; import R from 'ramda'; import { createQuery, compile, queryClass, PreAggregations, QueryFactory } from '@cubejs-backend/schema-compiler'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4, parse as uuidParse, stringify as uuidStringify } from 'uuid'; import { NativeInstance } from '@cubejs-backend/native'; export class CompilerApi { @@ -428,7 +428,7 @@ export class CompilerApi { const { cubeEvaluator } = compilers; if (!cubeEvaluator.isRbacEnabled()) { - return cubes; + return { cubes, visibilityMaskHash: null }; } for (const cube of cubes) { @@ -481,23 +481,38 @@ export class CompilerApi { }); }; - return cubes - .map((cube) => ({ - config: { - ...cube.config, - measures: cube.config.measures?.map(visibilityPatcherForCube(cube)), - dimensions: cube.config.dimensions?.map(visibilityPatcherForCube(cube)), - segments: cube.config.segments?.map(visibilityPatcherForCube(cube)), - hierarchies: cube.config.hierarchies?.map(visibilityPatcherForCube(cube)), - }, - })); + const visibiliyMask = JSON.stringify(isMemberVisibleInContext, Object.keys(isMemberVisibleInContext).sort()); + // This hash will be returned along the modified meta config and can be used + // to distinguish between different "schema versions" after DAP visibility is applied + const visibilityMaskHash = crypto.createHash('sha256').update(visibiliyMask).digest('hex'); + + return { + cubes: cubes + .map((cube) => ({ + config: { + ...cube.config, + measures: cube.config.measures?.map(visibilityPatcherForCube(cube)), + dimensions: cube.config.dimensions?.map(visibilityPatcherForCube(cube)), + segments: cube.config.segments?.map(visibilityPatcherForCube(cube)), + hierarchies: cube.config.hierarchies?.map(visibilityPatcherForCube(cube)), + }, + })), + visibilityMaskHash + }; + } + + mixInVisibilityMaskHash(compilerId, visibilityMaskHash) { + const uuidBytes = uuidParse(compilerId); + const hashBytes = Buffer.from(visibilityMaskHash, 'hex'); + return uuidv4({ random: crypto.createHash('sha256').update(uuidBytes).update(hashBytes).digest() + .subarray(0, 16) }); } async metaConfig(requestContext, options = {}) { const { includeCompilerId, ...restOptions } = options; const compilers = await this.getCompilers(restOptions); const { cubes } = compilers.metaTransformer; - const patchedCubes = await this.patchVisibilityByAccessPolicy( + const { visibilityMaskHash, cubes: patchedCubes } = await this.patchVisibilityByAccessPolicy( compilers, requestContext, cubes @@ -505,7 +520,11 @@ export class CompilerApi { if (includeCompilerId) { return { cubes: patchedCubes, - compilerId: compilers.compilerId, + // This compilerId is primarily used by the cubejs-backend-native or caching purposes. + // By default it doesn't account for member visibility changes introduced above by DAP. + // Here we're modifying the originila compilerId in a way that it's distinct for + // distinct schema versions while still being a valid UUID. + compilerId: visibilityMaskHash ? this.mixInVisibilityMaskHash(compilers.compilerId, visibilityMaskHash) : compilers.compilerId, }; } return patchedCubes; @@ -513,7 +532,7 @@ export class CompilerApi { async metaConfigExtended(requestContext, options) { const compilers = await this.getCompilers(options); - const patchedCubes = await this.patchVisibilityByAccessPolicy( + const { cubes: patchedCubes } = await this.patchVisibilityByAccessPolicy( compilers, requestContext, compilers.metaTransformer?.cubes diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js b/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js index e7c82f455e36f..6f50f842c97de 100644 --- a/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js @@ -1,5 +1,6 @@ module.exports = { contextToRoles: async (context) => context.securityContext.auth?.roles || [], + canSwitchSqlUser: async () => true, checkSqlAuth: async (req, user, password) => { if (user === 'admin') { if (password && password !== 'admin_password') { @@ -64,6 +65,27 @@ module.exports = { }, }; } + if (user === 'restricted') { + if (password && password !== 'restricted_password') { + throw new Error(`Password doesn't match for ${user}`); + } + return { + password, + superuser: false, + securityContext: { + auth: { + username: 'default', + userAttributes: { + region: 'CA', + city: 'San Francisco', + canHaveAdmin: true, + minDefaultId: 20000, + }, + roles: ['restricted'], + }, + }, + }; + } 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 index 7013052b0169e..8d35089fdbc43 100644 --- a/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/line_items.js +++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/line_items.js @@ -60,6 +60,12 @@ cube('line_items', { excludes: ['count', 'price', 'price_dim'], }, }, + { + role: 'restricted', + memberLevel: { + excludes: ['count', 'price', 'price_dim'], + }, + }, { role: 'admin', conditions: [ diff --git a/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap b/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap index b4a7ce5537530..645cd46bdd9ae 100644 --- a/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap +++ b/packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap @@ -770,3 +770,153 @@ Array [ `; exports[`Cube RBAC Engine RBAC via SQL API manager SELECT * from line_items: line_items_manager 1`] = `Array []`; + +exports[`Cube RBAC Engine RBAC via SQL changing users Switching user should allow more members to be visible: line_items 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-10-23T07:54:39.000Z, + "price": 267, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-01-01T13:50:20.000Z, + "price": 263, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-05-13T21:23:08.000Z, + "price": 180, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-04-10T22:51:15.000Z, + "price": 169, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2017-07-16T15:00:34.000Z, + "price": 156, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-05-23T04:25:27.000Z, + "price": 36, + "quantity": 5, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2018-09-29T20:29:30.000Z, + "price": 245, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-04-17T03:32:54.000Z, + "price": 232, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-11-15T18:22:17.000Z, + "price": 63, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "count": "1", + "created_at": 2019-12-16T08:09:36.000Z, + "price": 68, + "quantity": 6, + }, +] +`; + +exports[`Cube RBAC Engine RBAC via SQL changing users Switching user should allow more members to be visible: line_items_default 1`] = ` +Array [ + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2018-10-23T07:54:39.000Z, + "quantity": 2, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2018-01-01T13:50:20.000Z, + "quantity": 7, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2017-05-13T21:23:08.000Z, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2018-04-10T22:51:15.000Z, + "quantity": 6, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2017-07-16T15:00:34.000Z, + "quantity": 1, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2019-05-23T04:25:27.000Z, + "quantity": 5, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2018-09-29T20:29:30.000Z, + "quantity": 4, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2019-04-17T03:32:54.000Z, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2019-11-15T18:22:17.000Z, + "quantity": 8, + }, + Object { + "__cubeJoinField": null, + "__user": null, + "created_at": 2019-12-16T08:09:36.000Z, + "quantity": 6, + }, +] +`; diff --git a/packages/cubejs-testing/test/smoke-rbac.test.ts b/packages/cubejs-testing/test/smoke-rbac.test.ts index f318711932a8c..2d825ebf77609 100644 --- a/packages/cubejs-testing/test/smoke-rbac.test.ts +++ b/packages/cubejs-testing/test/smoke-rbac.test.ts @@ -198,6 +198,28 @@ describe('Cube RBAC Engine', () => { }); }); + describe('RBAC via SQL changing users', () => { + let connection: PgClient; + + beforeAll(async () => { + connection = await createPostgresClient('restricted', 'restricted_password'); + }); + + afterAll(async () => { + await connection.end(); + }, JEST_AFTER_ALL_DEFAULT_TIMEOUT); + + test('Switching user should allow more members to be visible', async () => { + const resDefault = await connection.query('SELECT * FROM line_items limit 10'); + expect(resDefault.rows).toMatchSnapshot('line_items_default'); + + await connection.query('SET USER=admin'); + + const resAdmin = await connection.query('SELECT * FROM line_items limit 10'); + expect(resAdmin.rows).toMatchSnapshot('line_items'); + }); + }); + describe('RBAC via REST API', () => { let client: CubeApi; let defaultClient: CubeApi; diff --git a/yarn.lock b/yarn.lock index e1fcc9fb89c53..6cb127ff844b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5306,6 +5306,18 @@ uuid "9.0.0" winston "3.8.2" +"@cubejs-backend/linter@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cubejs-backend/linter/-/linter-1.2.4.tgz#b38551ada107d94a3a12bb3f82e582ef6e184096" + integrity sha512-V150xWxBgRptCO4GAdZTuCwY7+FoxlJ1rBYM379LLEX8sLFC4NG0AN8S3vy0YSaUaC3P07ne3CrEILIJLt0JAg== + dependencies: + "@typescript-eslint/eslint-plugin" "^6.12.0" + "@typescript-eslint/parser" "^6.12.0" + eslint "^8.54.0" + eslint-config-airbnb-base "^14.2.1" + eslint-plugin-import "^2.22.1" + eslint-plugin-node "^9.2.0" + "@cubejs-backend/shared@0.33.20": version "0.33.20" resolved "https://registry.yarnpkg.com/@cubejs-backend/shared/-/shared-0.33.20.tgz#3d9fa60041599cca9fe4c04df05daa4b8ab8675f" @@ -5326,6 +5338,26 @@ throttle-debounce "^3.0.1" uuid "^8.3.2" +"@cubejs-backend/shared@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cubejs-backend/shared/-/shared-1.2.4.tgz#dcdc8a0537cfe9df97f3502bea983204634fbdfd" + integrity sha512-C/BA0ogCRhhtYwYyQkhXTTKLIUtJ1020aybbQ9tbKtlkrd3ab+a166jmtlAD5kDeoIhB08DgMTQjaQGDYh3gBg== + dependencies: + "@oclif/color" "^0.1.2" + bytes "^3.1.0" + cli-progress "^3.9.0" + cross-spawn "^7.0.3" + decompress "^4.2.1" + env-var "^6.3.0" + fs-extra "^9.1.0" + http-proxy-agent "^4.0.1" + moment-range "^4.0.1" + moment-timezone "^0.5.46" + node-fetch "^2.6.1" + shelljs "^0.8.5" + throttle-debounce "^3.0.1" + uuid "^8.3.2" + "@cubejs-infra/post-installer@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@cubejs-infra/post-installer/-/post-installer-0.0.7.tgz#a28d2d03e5b7b69a64020d75194a7078cf911d2d" @@ -27218,7 +27250,16 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -27316,7 +27357,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -27344,6 +27385,13 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.0, strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -29543,7 +29591,7 @@ workerpool@^9.2.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.2.0.tgz#f74427cbb61234708332ed8ab9cbf56dcb1c4371" integrity sha512-PKZqBOCo6CYkVOwAxWxQaSF2Fvb5Iv2fCeTP7buyWI2GiynWr46NcXSgK/idoV6e60dgCBfgYc+Un3HMvmqP8w== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -29569,6 +29617,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"