Skip to content

Commit 62f4bcd

Browse files
test: add YAML model tests for userAttributes shorthand in mask SQL
Add unit tests in yaml-schema.test.ts: - userAttributes shorthand in mask sql compiles and resolves correctly - user_attributes shorthand in mask sql compiles and resolves correctly - groups shorthand in mask sql compiles and resolves correctly Add YAML cube definition yaml_ua_mask_test in masking_test.yaml using userAttributes shorthand in mask.sql with CASE WHEN expression. Add smoke tests for the YAML-defined mask (SQL + REST, both non-Tesseract and Tesseract). Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
1 parent bbf6d22 commit 62f4bcd

3 files changed

Lines changed: 192 additions & 0 deletions

File tree

packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { prepareYamlCompiler } from './PrepareCompiler';
2+
import { PostgresQuery } from '../../src';
23

34
describe('Yaml Schema Testing', () => {
45
describe('Duplicate member detection', () => {
@@ -1114,4 +1115,122 @@ cubes:
11141115
}
11151116
});
11161117
});
1118+
1119+
describe('Mask SQL with shorthand', () => {
1120+
it('userAttributes shorthand in mask sql should compile and resolve', async () => {
1121+
const compilers = prepareYamlCompiler(`
1122+
cubes:
1123+
- name: orders
1124+
sql_table: public.orders
1125+
dimensions:
1126+
- name: id
1127+
sql: id
1128+
type: number
1129+
primary_key: true
1130+
- name: status
1131+
sql: status
1132+
type: string
1133+
mask:
1134+
sql: "CASE WHEN { userAttributes.hasStatusAccess } THEN {CUBE}.status ELSE '***' END"
1135+
measures:
1136+
- name: count
1137+
type: count
1138+
access_policy:
1139+
- role: "*"
1140+
member_level:
1141+
includes: []
1142+
member_masking:
1143+
includes: "*"
1144+
`);
1145+
1146+
await compilers.compiler.compile();
1147+
1148+
const dim = compilers.cubeEvaluator.cubeFromPath('orders').dimensions.status;
1149+
const maskSql = (dim as any).mask.sql.toString();
1150+
expect(maskSql).toContain('SECURITY_CONTEXT.cubeCloud.userAttributes.hasStatusAccess');
1151+
expect(maskSql).toContain('CUBE');
1152+
expect(maskSql).not.toMatch(/[^.}]userAttributes\.hasStatusAccess/);
1153+
1154+
const query = new PostgresQuery(
1155+
compilers,
1156+
{
1157+
measures: ['orders.count'],
1158+
dimensions: ['orders.status'],
1159+
maskedMembers: ['orders.status'],
1160+
contextSymbols: {
1161+
securityContext: { cubeCloud: { userAttributes: { hasStatusAccess: true } } }
1162+
}
1163+
}
1164+
);
1165+
const sql = query.buildSqlAndParams();
1166+
expect(sql[0]).toContain('"orders".status');
1167+
expect(sql[0]).toContain('CASE WHEN');
1168+
});
1169+
1170+
it('user_attributes shorthand in mask sql should compile and resolve', async () => {
1171+
const compilers = prepareYamlCompiler(`
1172+
cubes:
1173+
- name: orders
1174+
sql_table: public.orders
1175+
dimensions:
1176+
- name: id
1177+
sql: id
1178+
type: number
1179+
primary_key: true
1180+
- name: status
1181+
sql: status
1182+
type: string
1183+
mask:
1184+
sql: "CASE WHEN { user_attributes.hasStatusAccess } THEN {CUBE}.status ELSE '***' END"
1185+
measures:
1186+
- name: count
1187+
type: count
1188+
access_policy:
1189+
- role: "*"
1190+
member_level:
1191+
includes: []
1192+
member_masking:
1193+
includes: "*"
1194+
`);
1195+
1196+
await compilers.compiler.compile();
1197+
1198+
const dim = compilers.cubeEvaluator.cubeFromPath('orders').dimensions.status;
1199+
const maskSql = (dim as any).mask.sql.toString();
1200+
expect(maskSql).toContain('SECURITY_CONTEXT.cubeCloud.userAttributes.hasStatusAccess');
1201+
});
1202+
1203+
it('groups shorthand in mask sql should compile and resolve', async () => {
1204+
const compilers = prepareYamlCompiler(`
1205+
cubes:
1206+
- name: orders
1207+
sql_table: public.orders
1208+
dimensions:
1209+
- name: id
1210+
sql: id
1211+
type: number
1212+
primary_key: true
1213+
- name: secret
1214+
sql: price
1215+
type: number
1216+
mask:
1217+
sql: "CASE WHEN {CUBE}.product_id IN ({groups}) THEN {CUBE}.price ELSE -1 END"
1218+
measures:
1219+
- name: count
1220+
type: count
1221+
access_policy:
1222+
- role: "*"
1223+
member_level:
1224+
includes: []
1225+
member_masking:
1226+
includes: "*"
1227+
`);
1228+
1229+
await compilers.compiler.compile();
1230+
1231+
const dim = compilers.cubeEvaluator.cubeFromPath('orders').dimensions.secret;
1232+
const maskSql = (dim as any).mask.sql.toString();
1233+
expect(maskSql).toContain('SECURITY_CONTEXT.cubeCloud.groups');
1234+
});
1235+
});
11171236
});

packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/masking_test.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,32 @@ cubes:
106106
member_level:
107107
includes: []
108108

109+
- name: yaml_ua_mask_test
110+
sql_table: public.line_items
111+
112+
dimensions:
113+
- name: id
114+
sql: id
115+
type: number
116+
primary_key: true
117+
118+
- name: masked_status
119+
sql: product_id
120+
type: number
121+
mask:
122+
sql: "CASE WHEN { userAttributes.tenantId } = '1' THEN {CUBE}.product_id ELSE -1 END"
123+
124+
measures:
125+
- name: count
126+
type: count
127+
128+
access_policy:
129+
- role: "*"
130+
member_level:
131+
includes: []
132+
member_masking:
133+
includes: "*"
134+
109135
views:
110136
# View with full access at view level - but cube masking still applies (RLS pattern)
111137
# Excludes members with {CUBE} references in SQL (secret_string, secret_boolean)

packages/cubejs-testing/test/smoke-rbac.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,18 @@ describe('Cube RBAC Engine', () => {
843843
expect(row.masked_product).toBeLessThan(0);
844844
}
845845
});
846+
847+
test('userAttributes shorthand in YAML mask sql', async () => {
848+
const res = await connection.query(
849+
'SELECT * FROM yaml_ua_mask_test LIMIT 5'
850+
);
851+
expect(res.rows.length).toBeGreaterThan(0);
852+
for (const row of res.rows) {
853+
// sc_test user has tenantId = '1', so the CASE WHEN evaluates to true
854+
// and masked_status should be the actual product_id (positive)
855+
expect(row.masked_status).toBeGreaterThan(0);
856+
}
857+
});
846858
});
847859

848860
describe('SECURITY_CONTEXT.cubeCloud features via REST API', () => {
@@ -933,6 +945,19 @@ describe('Cube RBAC Engine', () => {
933945
expect(row['sc_cube_mask_test.masked_product']).toBeLessThan(0);
934946
}
935947
});
948+
949+
test('userAttributes shorthand in YAML mask sql via REST', async () => {
950+
const result = await scClient.load({
951+
measures: ['yaml_ua_mask_test.count'],
952+
dimensions: ['yaml_ua_mask_test.masked_status'],
953+
});
954+
const rows = result.rawData();
955+
expect(rows.length).toBeGreaterThan(0);
956+
for (const row of rows) {
957+
// sc_test user has tenantId = '1', so masked_status should be actual product_id
958+
expect(row['yaml_ua_mask_test.masked_status']).toBeGreaterThan(0);
959+
}
960+
});
936961
});
937962

938963
describe('RBAC via REST API', () => {
@@ -1097,6 +1122,16 @@ describe('Cube RBAC Engine [Tesseract]', () => {
10971122
expect(row.masked_product).toBeLessThan(0);
10981123
}
10991124
});
1125+
1126+
test('userAttributes shorthand in YAML mask sql', async () => {
1127+
const res = await connection.query(
1128+
'SELECT * FROM yaml_ua_mask_test LIMIT 5'
1129+
);
1130+
expect(res.rows.length).toBeGreaterThan(0);
1131+
for (const row of res.rows) {
1132+
expect(row.masked_status).toBeGreaterThan(0);
1133+
}
1134+
});
11001135
});
11011136

11021137
describe('Shorthand and mask tests via REST API [Tesseract]', () => {
@@ -1148,6 +1183,18 @@ describe('Cube RBAC Engine [Tesseract]', () => {
11481183
expect(row['sc_cube_mask_test.masked_product']).toBeLessThan(0);
11491184
}
11501185
});
1186+
1187+
test('userAttributes shorthand in YAML mask sql via REST', async () => {
1188+
const result = await scClient.load({
1189+
measures: ['yaml_ua_mask_test.count'],
1190+
dimensions: ['yaml_ua_mask_test.masked_status'],
1191+
});
1192+
const rows = result.rawData();
1193+
expect(rows.length).toBeGreaterThan(0);
1194+
for (const row of rows) {
1195+
expect(row['yaml_ua_mask_test.masked_status']).toBeGreaterThan(0);
1196+
}
1197+
});
11511198
});
11521199
});
11531200

0 commit comments

Comments
 (0)