diff --git a/docs-mintlify/reference/data-modeling/data-access-policies.mdx b/docs-mintlify/reference/data-modeling/data-access-policies.mdx index 06cee4fb4dab4..24b63e1a8648d 100644 --- a/docs-mintlify/reference/data-modeling/data-access-policies.mdx +++ b/docs-mintlify/reference/data-modeling/data-access-policies.mdx @@ -214,7 +214,8 @@ The `if` expression must evaluate to a boolean. The syntax you can use inside it depends on the data model format: - In **YAML** data models, the expression inside `{ }` is evaluated as a Python -expression. Only logical operators, member access, and method calls are supported. +expression. Logical operators, equality and inequality, member access, and method +calls are supported. - In **JavaScript** data models, the expression is evaluated as a JavaScript expression, so a broader set of operators is available. @@ -227,12 +228,15 @@ The following operators and constructs are supported: | Logical NOT | `not` | `!` | | Member access | `.` | `.` | | Method call (e.g., `includes`) | `.includes(…)` | `.includes(…)` | -| Equality and inequality | — | `===`, `!==`, `==`, `!=` | +| Equality and inequality | `==`, `!=` | `===`, `!==`, `==`, `!=` | | Comparison | — | `<`, `>`, `<=`, `>=` | | Arithmetic | — | `+`, `-`, `*`, `/`, `%`, `**` | | Ternary | — | `? :` | | Optional chaining | — | `?.` | +Chained comparisons aren't supported in YAML. Write `a == b and b == c` instead +of `a == b == c`. + For example, the following policy applies to users who are not blocked, are either administrators or based in the EMEA region, and belong to the `admins` group. Each requirement is expressed as a separate condition, and all conditions must be `true` diff --git a/packages/cubejs-schema-compiler/src/parser/PythonParser.ts b/packages/cubejs-schema-compiler/src/parser/PythonParser.ts index 572509f78e696..b56a23077e48d 100644 --- a/packages/cubejs-schema-compiler/src/parser/PythonParser.ts +++ b/packages/cubejs-schema-compiler/src/parser/PythonParser.ts @@ -24,6 +24,9 @@ import Python3Parser, { And_testContext, // eslint-disable-next-line camelcase Or_testContext, + ComparisonContext, + // eslint-disable-next-line camelcase + Comp_opContext, } from './Python3Parser'; import { UserError } from '../compiler/UserError'; import Python3ParserVisitor from './Python3ParserVisitor'; @@ -244,6 +247,21 @@ export class PythonParser { return children[0]; } return children.reduce((left, right) => t.logicalExpression('||', left, right)); + } else if (node instanceof Comp_opContext) { + if (node.EQUALS()) { + return '==='; + } else if (node.NOT_EQ_2()) { + return '!=='; + } + throw new UserError(`Unsupported Python comparison operator: ${node.getText()}`); + } else if (node instanceof ComparisonContext) { + if (children.length === 1) { + return children[0]; + } + if (children.length !== 3) { + throw new UserError('Chained Python comparisons are not supported'); + } + return t.binaryExpression(children[1], children[0], children[2]); } else if (node instanceof ArglistContext) { return children; } else { 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 265c1c93a43e4..20d142273b685 100644 --- a/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/yaml-schema.test.ts @@ -839,6 +839,71 @@ cubes: }); describe('Access policy: ', () => { + it('supports equality and inequality operators in conditions', async () => { + const compilers = prepareYamlCompiler( + ` + cubes: + - name: Orders + sql: "select * from tbl" + measures: + - name: count + type: count + accessPolicy: + - role: admin + conditions: + - if: "{ securityContext.region == 'EMEA' }" + - if: "{ securityContext.department != 'Sales' }" + ` + ); + + await compilers.compiler.compile(); + + const conditions = compilers.cubeEvaluator.cubeFromPath('Orders').accessPolicy![0].conditions!; + expect(conditions[0].if.toString()).toContain('securityContext.region === "EMEA"'); + expect(conditions[1].if.toString()).toContain('securityContext.department !== "Sales"'); + expect(conditions[0].if({ region: 'EMEA' })).toBe(true); + expect(conditions[1].if({ department: 'Sales' })).toBe(false); + expect(conditions[1].if({ department: 'Marketing' })).toBe(true); + }); + + it('does not support chained comparisons in conditions', async () => { + const { compiler } = prepareYamlCompiler( + ` + cubes: + - name: Orders + sql: "select * from tbl" + measures: + - name: count + type: count + accessPolicy: + - role: admin + conditions: + - if: "{ securityContext.region == securityContext.department == 'Sales' }" + ` + ); + + await expect(compiler.compile()).rejects.toThrow('Chained Python comparisons are not supported'); + }); + + it.each(['===', '!=='])('does not support JavaScript operator %s in conditions', async (operator) => { + const { compiler } = prepareYamlCompiler( + ` + cubes: + - name: Orders + sql: "select * from tbl" + measures: + - name: count + type: count + accessPolicy: + - role: admin + conditions: + - if: "{ securityContext.region ${operator} 'EMEA' }" + ` + ); + + await expect(compiler.compile()).rejects.toThrow('Failed to parse Python expression'); + }); + it('defines a correct accessPolicy', async () => { const { compiler } = prepareYamlCompiler( `