Skip to content

Commit 6d9afac

Browse files
authored
fix: SECURITY_CONTEXT renders empty strings on filter with enabled Tesseract (#10576)
* fix: SECURITY_CONTEXT is not working correctly with enabled Tesseract * IN filter fix, toString and cloud shortcuts * Linter * Smoke tests * Test fixes * Test fixes * Test fixes * Test fixes * Shorthand precedence * Fix tests * More tests and more precise access policy match
1 parent 97ad522 commit 6d9afac

9 files changed

Lines changed: 1173 additions & 79 deletions

File tree

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,35 +1480,79 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
14801480
}
14811481

14821482
public static contextSymbolsProxyFrom(symbols: object, allocateParam: (param: unknown) => unknown): object {
1483-
return new Proxy(symbols, {
1484-
get: (target, name) => {
1485-
const propValue = target[name];
1486-
const methods = (paramValue) => ({
1487-
filter: (column) => {
1488-
if (paramValue) {
1489-
const value = Array.isArray(paramValue) ?
1490-
paramValue.map(allocateParam) :
1491-
allocateParam(paramValue);
1492-
if (typeof column === 'function') {
1493-
return column(value);
1494-
} else {
1495-
return `${column} = ${value}`;
1496-
}
1483+
const methods = (paramValue) => ({
1484+
filter: (column) => {
1485+
if (paramValue) {
1486+
if (Array.isArray(paramValue)) {
1487+
const values = paramValue.map(allocateParam);
1488+
if (typeof column === 'function') {
1489+
return column(values);
14971490
} else {
1498-
return '1 = 1';
1491+
return `${column} IN (${values.join(', ')})`;
14991492
}
1500-
},
1501-
requiredFilter: (column) => {
1502-
if (!paramValue) {
1503-
throw new UserError(`Filter for ${column} is required`);
1493+
} else {
1494+
const value = allocateParam(paramValue);
1495+
if (typeof column === 'function') {
1496+
return column(value);
1497+
} else {
1498+
return `${column} = ${value}`;
15041499
}
1505-
return methods(paramValue).filter(column);
1506-
},
1507-
unsafeValue: () => paramValue
1508-
});
1509-
return methods(target)[name] ||
1510-
typeof propValue === 'object' && propValue !== null && CubeSymbols.contextSymbolsProxyFrom(propValue, allocateParam) ||
1511-
methods(propValue);
1500+
}
1501+
} else {
1502+
return '1 = 1';
1503+
}
1504+
},
1505+
requiredFilter: (column) => {
1506+
if (!paramValue) {
1507+
throw new UserError(`Filter for ${column} is required`);
1508+
}
1509+
return methods(paramValue).filter(column);
1510+
},
1511+
unsafeValue: () => paramValue,
1512+
toString: () => {
1513+
if (paramValue !== undefined && paramValue !== null) {
1514+
return Array.isArray(paramValue)
1515+
? paramValue.map(allocateParam).join(',')
1516+
: String(allocateParam(paramValue));
1517+
}
1518+
return '';
1519+
},
1520+
[Symbol.toPrimitive]: () => {
1521+
if (paramValue !== undefined && paramValue !== null) {
1522+
return Array.isArray(paramValue)
1523+
? paramValue.map(allocateParam).join(',')
1524+
: String(allocateParam(paramValue));
1525+
}
1526+
return '';
1527+
}
1528+
});
1529+
1530+
// Chainable proxy for undefined/null values: supports both method calls
1531+
// (filter, unsafeValue, etc.) and further property chaining for deeply
1532+
// nested paths like SECURITY_CONTEXT.cubeCloud.tenantId.filter(...)
1533+
// when the security context is empty during compilation/dep resolution.
1534+
const undefinedChainableHandler: ProxyHandler<any> = {
1535+
get: (target, name) => {
1536+
if (name in target || typeof name === 'symbol') return target[name];
1537+
return new Proxy(methods(undefined), undefinedChainableHandler);
1538+
}
1539+
};
1540+
1541+
return new Proxy(symbols, {
1542+
get: (target, name) => {
1543+
const propValue = target[name];
1544+
const methodOnTarget = methods(target)[name];
1545+
if (methodOnTarget) return methodOnTarget;
1546+
1547+
if (typeof propValue === 'object' && propValue !== null) {
1548+
return CubeSymbols.contextSymbolsProxyFrom(propValue, allocateParam);
1549+
}
1550+
1551+
if (propValue !== undefined && propValue !== null) {
1552+
return methods(propValue);
1553+
}
1554+
1555+
return new Proxy(methods(undefined), undefinedChainableHandler);
15121556
}
15131557
});
15141558
}

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -214,17 +214,22 @@ export class CubePropContextTranspiler implements TranspilerInterface {
214214

215215
protected static collectKnownIdentifiersAndTransform(resolveSymbol: SymbolResolver, path: NodePath): string[] {
216216
const identifiers: string[] = [];
217+
const isAccessPolicy = this.isAccessPolicyPath(path);
217218

218219
if (path.node.type === 'Identifier') {
219-
CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers);
220+
CubePropContextTranspiler.transformCubeCloudShorthandIdentifier(path as NodePath<t.Identifier>, identifiers, isAccessPolicy, resolveSymbol);
221+
if (path.node.type === 'Identifier') {
222+
CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers);
223+
}
220224
}
221225

222226
path.traverse({
223227
Identifier: (p) => {
228+
CubePropContextTranspiler.transformCubeCloudShorthandIdentifier(p, identifiers, isAccessPolicy, resolveSymbol);
224229
CubePropContextTranspiler.matchAndTransformIdentifier(p, resolveSymbol, identifiers);
225230
},
226231
MemberExpression: (p) => {
227-
CubePropContextTranspiler.transformUserAttributesMemberExpression(p);
232+
CubePropContextTranspiler.transformCubeCloudShorthandMemberExpression(p, isAccessPolicy, resolveSymbol);
228233
}
229234
});
230235

@@ -238,39 +243,83 @@ export class CubePropContextTranspiler implements TranspilerInterface {
238243
) &&
239244
resolveSymbol(path.node.name)
240245
) {
241-
// Special handling for userAttributes - replace in parameter list with securityContext
242-
const fullPath = this.fullPath(path);
243-
if ((path.node.name === 'userAttributes' || path.node.name === 'user_attributes') && (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))) {
244-
identifiers.push('securityContext');
245-
} else {
246-
identifiers.push(path.node.name);
246+
identifiers.push(path.node.name);
247+
}
248+
}
249+
250+
private static readonly CUBE_CLOUD_SHORTHAND_IDENTIFIERS = ['userAttributes', 'user_attributes', 'groups'];
251+
252+
private static isAccessPolicyPath(path: NodePath): boolean {
253+
// @ts-ignore
254+
const target = (!path?.node?.key && path?.parentPath && t.isObjectProperty(path.parentPath.node))
255+
? path.parentPath
256+
: path;
257+
const fp = this.fullPath(target);
258+
return fp.startsWith('accessPolicy') || fp.startsWith('access_policy');
259+
}
260+
261+
private static securityContextIdentifier(isAccessPolicy: boolean): t.Identifier {
262+
return t.identifier(isAccessPolicy ? 'securityContext' : 'SECURITY_CONTEXT');
263+
}
264+
265+
private static isShadowedByFunctionParam(name: string, path: NodePath): boolean {
266+
let current: NodePath | null = path.parentPath;
267+
while (current) {
268+
const { node } = current;
269+
if (
270+
(t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) &&
271+
node.params.some(p => t.isIdentifier(p) && p.name === name)
272+
) {
273+
return true;
247274
}
275+
current = current.parentPath;
248276
}
277+
return false;
249278
}
250279

251-
protected static transformUserAttributesMemberExpression(path: NodePath<t.MemberExpression>) {
252-
// Check if this is userAttributes.someProperty (object should be identifier named 'userAttributes')
253-
const fullPath = this.fullPath(path);
280+
protected static transformCubeCloudShorthandIdentifier(path: NodePath<t.Identifier>, identifiers: string[], isAccessPolicy: boolean, resolveSymbol: SymbolResolver) {
281+
if (!this.CUBE_CLOUD_SHORTHAND_IDENTIFIERS.includes(path.node.name)) {
282+
return;
283+
}
284+
if (resolveSymbol(path.node.name)) {
285+
return;
286+
}
254287
if (
255-
(t.isIdentifier(path.node.object, { name: 'userAttributes' }) || t.isIdentifier(path.node.object, { name: 'user_attributes' })) &&
256-
(fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))
288+
path.parent &&
289+
(path.parent.type === 'MemberExpression' || path.parent.type === 'OptionalMemberExpression') &&
290+
path.key === 'property'
257291
) {
258-
// Replace userAttributes with securityContext.cubeCloud.userAttributes
259-
const securityContext = t.identifier('securityContext');
260-
const cubeCloud = t.memberExpression(securityContext, t.identifier('cubeCloud'));
261-
const userAttributes = t.memberExpression(cubeCloud, t.identifier('userAttributes'));
262-
const newMemberExpression = t.memberExpression(userAttributes, path.node.property, path.node.computed);
292+
return;
293+
}
294+
if (this.isShadowedByFunctionParam(path.node.name, path)) {
295+
return;
296+
}
297+
const contextId = this.securityContextIdentifier(isAccessPolicy);
298+
const cubeCloud = t.memberExpression(contextId, t.identifier('cubeCloud'));
299+
const prop = path.node.name === 'user_attributes' ? 'userAttributes' : path.node.name;
300+
const newExpr = t.memberExpression(cubeCloud, t.identifier(prop));
301+
path.replaceWith(newExpr);
302+
identifiers.push(contextId.name);
303+
}
263304

305+
protected static transformCubeCloudShorthandMemberExpression(path: NodePath<t.MemberExpression>, isAccessPolicy: boolean, resolveSymbol: SymbolResolver) {
306+
if (
307+
t.isIdentifier(path.node.object) &&
308+
this.CUBE_CLOUD_SHORTHAND_IDENTIFIERS.includes(path.node.object.name) &&
309+
!resolveSymbol(path.node.object.name) &&
310+
!this.isShadowedByFunctionParam(path.node.object.name, path)
311+
) {
312+
const contextId = this.securityContextIdentifier(isAccessPolicy);
313+
const cubeCloud = t.memberExpression(contextId, t.identifier('cubeCloud'));
314+
const prop = path.node.object.name === 'user_attributes' ? 'userAttributes' : path.node.object.name;
315+
const shorthand = t.memberExpression(cubeCloud, t.identifier(prop));
316+
const newMemberExpression = t.memberExpression(shorthand, path.node.property, path.node.computed);
264317
path.replaceWith(newMemberExpression);
265318
} else if (
266-
t.isMemberExpression(path.node.object) &&
319+
(t.isMemberExpression(path.node.object) || t.isOptionalMemberExpression(path.node.object)) &&
267320
t.isIdentifier(path.node.object.property, { name: 'user_attributes' }) &&
268-
!path.node.object.computed &&
269-
(fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))
321+
!path.node.object.computed
270322
) {
271-
// Also handle case where user_attributes appears within a MemberExpression chain like
272-
// securityContext.cubeCloud.user_attributes
273-
// We need to convert user_attributes to userAttributes in such chains
274323
const newObject = t.memberExpression(
275324
path.node.object.object,
276325
t.identifier('userAttributes'),

0 commit comments

Comments
 (0)