Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions packages/cubejs-server-core/src/core/CompilerApi.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -428,7 +428,7 @@ export class CompilerApi {
const { cubeEvaluator } = compilers;

if (!cubeEvaluator.isRbacEnabled()) {
return cubes;
return { cubes, visibilityMaskHash: null };
}

for (const cube of cubes) {
Expand Down Expand Up @@ -481,39 +481,58 @@ 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
);
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;
}

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
Expand Down
22 changes: 22 additions & 0 deletions packages/cubejs-testing/birdbox-fixtures/rbac/cube.js
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down Expand Up @@ -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`);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ cube('line_items', {
excludes: ['count', 'price', 'price_dim'],
},
},
{
role: 'restricted',
memberLevel: {
excludes: ['count', 'price', 'price_dim'],
},
},
{
role: 'admin',
conditions: [
Expand Down
150 changes: 150 additions & 0 deletions packages/cubejs-testing/test/__snapshots__/smoke-rbac.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
]
`;
22 changes: 22 additions & 0 deletions packages/cubejs-testing/test/smoke-rbac.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,28 @@
});
});

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;
Expand Down
Loading
Loading