diff --git a/docs-mintlify/admin/users-and-permissions/index.mdx b/docs-mintlify/admin/users-and-permissions/index.mdx
deleted file mode 100644
index 1fa8c9904bd48..0000000000000
--- a/docs-mintlify/admin/users-and-permissions/index.mdx
+++ /dev/null
@@ -1,19 +0,0 @@
----
-description: Manage user attributes, roles, permissions, and groups to control access to Cube features and data.
----
-
- Users and permissions
-
-Manage users and control access to features and data in Cube.
-
-## Users and permissions
-
-Configure access control to:
-
-- Define [user attributes][ref-attributes] for personalized data access
-- Set up [roles and permissions][ref-roles] to control feature access
-- Create [custom roles][ref-custom-roles] with specific permissions for your team
-
-[ref-attributes]: /admin/users-and-permissions/user-attributes
-[ref-roles]: /admin/users-and-permissions/roles-and-permissions
-[ref-custom-roles]: /admin/users-and-permissions/custom-roles
\ No newline at end of file
diff --git a/docs-mintlify/admin/users-and-permissions/manage-users.mdx b/docs-mintlify/admin/users-and-permissions/manage-users.mdx
new file mode 100644
index 0000000000000..0bc97d85964ac
--- /dev/null
+++ b/docs-mintlify/admin/users-and-permissions/manage-users.mdx
@@ -0,0 +1,119 @@
+---
+title: Manage users
+description: Add users to your Cube Cloud account, assign roles, and control their access.
+---
+
+Use the **Admin → Users** page to add people to your Cube Cloud account,
+change their roles, and control what they can access.
+
+
+
+Only users with the Admin role can manage other users.
+
+
+
+## The user list
+
+The user list displays all users in your account along with their roles
+and status. Use the search bar to filter users by name or email.
+
+## Inviting users
+
+To invite a new user:
+
+1. Navigate to **Admin → Users**.
+2. Click **Add User**.
+3. Enter the user's email address.
+4. Select a [role][ref-roles] for the user: Admin, Developer, Explorer, or
+ Viewer.
+5. Optionally, assign one or more [custom roles][ref-custom-roles].
+6. Click **Create** to send the invitation.
+
+After the user is created, an invitation link is generated. You can copy the
+link and share it with the user. The user must visit the link to set their
+password and activate their account.
+
+### Resending invitations
+
+If a user hasn't activated their account, you can resend or copy the
+invitation link from the user list.
+
+## Managing individual users
+
+Click on a user in the user list to access their settings page. From here,
+you can:
+
+- Update the user's name
+- Change their [role][ref-roles] (Admin, Developer, Explorer, or Viewer)
+- Assign or remove [custom roles][ref-custom-roles]
+- Add the user to [user groups][ref-groups]
+- Set [user attribute][ref-attributes] values for data access control
+
+### Changing a user's role
+
+To change a user's role:
+
+1. Navigate to **Admin → Users** and click on the user.
+2. Select a new role from the role dropdown.
+3. Save the changes.
+
+Alternatively, you can change a user's role directly from the user list
+using the role dropdown.
+
+
+
+Admin roles are billed at the developer rate.
+
+
+
+## Deactivating and reactivating users
+
+Deactivating a user revokes their access to Cube Cloud without permanently
+removing their account. The user's active sessions are terminated immediately.
+
+To deactivate a user:
+
+1. Navigate to **Admin → Users**.
+2. Click the actions menu for the user.
+3. Select **Deactivate**.
+
+
+
+You cannot deactivate yourself or the last active Admin user.
+
+
+
+To reactivate a deactivated user, follow the same steps and select
+**Activate**. The user can then log in again with their existing credentials.
+
+## Deleting users
+
+Deleting a user permanently removes their account from Cube Cloud.
+
+To delete a user:
+
+1. Navigate to **Admin → Users**.
+2. Click the actions menu for the user.
+3. Select **Delete**.
+
+
+
+You cannot delete your own account. Deleting a user is irreversible.
+
+
+
+## Provisioning users via SCIM
+
+If your organization uses an identity provider such as [Okta][ref-okta] or
+[Microsoft Entra ID][ref-entra-id], you can automate user provisioning and
+deprovisioning through SCIM. See the [SSO & Identity Providers][ref-sso]
+documentation for setup instructions.
+
+
+[ref-roles]: /admin/users-and-permissions/roles-and-permissions
+[ref-custom-roles]: /admin/users-and-permissions/custom-roles
+[ref-attributes]: /admin/users-and-permissions/user-attributes
+[ref-groups]: /admin/users-and-permissions/user-groups
+[ref-sso]: /admin/sso
+[ref-okta]: /admin/sso/okta/scim
+[ref-entra-id]: /admin/sso/microsoft-entra-id/scim
diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json
index 67b4a81418dc2..8de0e22ae4bb3 100644
--- a/docs-mintlify/docs.json
+++ b/docs-mintlify/docs.json
@@ -205,7 +205,7 @@
{
"group": "Users & Permissions",
"pages": [
- "admin/users-and-permissions",
+ "admin/users-and-permissions/manage-users",
"admin/users-and-permissions/roles-and-permissions",
"admin/users-and-permissions/user-attributes",
"admin/users-and-permissions/user-groups",
diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts
index 655ef53e20056..33c2f5f3b6f4a 100644
--- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts
+++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts
@@ -1480,35 +1480,79 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
}
public static contextSymbolsProxyFrom(symbols: object, allocateParam: (param: unknown) => unknown): object {
- return new Proxy(symbols, {
- get: (target, name) => {
- const propValue = target[name];
- const methods = (paramValue) => ({
- filter: (column) => {
- if (paramValue) {
- const value = Array.isArray(paramValue) ?
- paramValue.map(allocateParam) :
- allocateParam(paramValue);
- if (typeof column === 'function') {
- return column(value);
- } else {
- return `${column} = ${value}`;
- }
+ const methods = (paramValue) => ({
+ filter: (column) => {
+ if (paramValue) {
+ if (Array.isArray(paramValue)) {
+ const values = paramValue.map(allocateParam);
+ if (typeof column === 'function') {
+ return column(values);
} else {
- return '1 = 1';
+ return `${column} IN (${values.join(', ')})`;
}
- },
- requiredFilter: (column) => {
- if (!paramValue) {
- throw new UserError(`Filter for ${column} is required`);
+ } else {
+ const value = allocateParam(paramValue);
+ if (typeof column === 'function') {
+ return column(value);
+ } else {
+ return `${column} = ${value}`;
}
- return methods(paramValue).filter(column);
- },
- unsafeValue: () => paramValue
- });
- return methods(target)[name] ||
- typeof propValue === 'object' && propValue !== null && CubeSymbols.contextSymbolsProxyFrom(propValue, allocateParam) ||
- methods(propValue);
+ }
+ } else {
+ return '1 = 1';
+ }
+ },
+ requiredFilter: (column) => {
+ if (!paramValue) {
+ throw new UserError(`Filter for ${column} is required`);
+ }
+ return methods(paramValue).filter(column);
+ },
+ unsafeValue: () => paramValue,
+ toString: () => {
+ if (paramValue !== undefined && paramValue !== null) {
+ return Array.isArray(paramValue)
+ ? paramValue.map(allocateParam).join(',')
+ : String(allocateParam(paramValue));
+ }
+ return '';
+ },
+ [Symbol.toPrimitive]: () => {
+ if (paramValue !== undefined && paramValue !== null) {
+ return Array.isArray(paramValue)
+ ? paramValue.map(allocateParam).join(',')
+ : String(allocateParam(paramValue));
+ }
+ return '';
+ }
+ });
+
+ // Chainable proxy for undefined/null values: supports both method calls
+ // (filter, unsafeValue, etc.) and further property chaining for deeply
+ // nested paths like SECURITY_CONTEXT.cubeCloud.tenantId.filter(...)
+ // when the security context is empty during compilation/dep resolution.
+ const undefinedChainableHandler: ProxyHandler = {
+ get: (target, name) => {
+ if (name in target || typeof name === 'symbol') return target[name];
+ return new Proxy(methods(undefined), undefinedChainableHandler);
+ }
+ };
+
+ return new Proxy(symbols, {
+ get: (target, name) => {
+ const propValue = target[name];
+ const methodOnTarget = methods(target)[name];
+ if (methodOnTarget) return methodOnTarget;
+
+ if (typeof propValue === 'object' && propValue !== null) {
+ return CubeSymbols.contextSymbolsProxyFrom(propValue, allocateParam);
+ }
+
+ if (propValue !== undefined && propValue !== null) {
+ return methods(propValue);
+ }
+
+ return new Proxy(methods(undefined), undefinedChainableHandler);
}
});
}
diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts
index 06beef569bc6b..d007111181876 100644
--- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts
+++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts
@@ -214,17 +214,22 @@ export class CubePropContextTranspiler implements TranspilerInterface {
protected static collectKnownIdentifiersAndTransform(resolveSymbol: SymbolResolver, path: NodePath): string[] {
const identifiers: string[] = [];
+ const isAccessPolicy = this.isAccessPolicyPath(path);
if (path.node.type === 'Identifier') {
- CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers);
+ CubePropContextTranspiler.transformCubeCloudShorthandIdentifier(path as NodePath, identifiers, isAccessPolicy, resolveSymbol);
+ if (path.node.type === 'Identifier') {
+ CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers);
+ }
}
path.traverse({
Identifier: (p) => {
+ CubePropContextTranspiler.transformCubeCloudShorthandIdentifier(p, identifiers, isAccessPolicy, resolveSymbol);
CubePropContextTranspiler.matchAndTransformIdentifier(p, resolveSymbol, identifiers);
},
MemberExpression: (p) => {
- CubePropContextTranspiler.transformUserAttributesMemberExpression(p);
+ CubePropContextTranspiler.transformCubeCloudShorthandMemberExpression(p, isAccessPolicy, resolveSymbol);
}
});
@@ -238,39 +243,83 @@ export class CubePropContextTranspiler implements TranspilerInterface {
) &&
resolveSymbol(path.node.name)
) {
- // Special handling for userAttributes - replace in parameter list with securityContext
- const fullPath = this.fullPath(path);
- if ((path.node.name === 'userAttributes' || path.node.name === 'user_attributes') && (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))) {
- identifiers.push('securityContext');
- } else {
- identifiers.push(path.node.name);
+ identifiers.push(path.node.name);
+ }
+ }
+
+ private static readonly CUBE_CLOUD_SHORTHAND_IDENTIFIERS = ['userAttributes', 'user_attributes', 'groups'];
+
+ private static isAccessPolicyPath(path: NodePath): boolean {
+ // @ts-ignore
+ const target = (!path?.node?.key && path?.parentPath && t.isObjectProperty(path.parentPath.node))
+ ? path.parentPath
+ : path;
+ const fp = this.fullPath(target);
+ return fp.startsWith('accessPolicy') || fp.startsWith('access_policy');
+ }
+
+ private static securityContextIdentifier(isAccessPolicy: boolean): t.Identifier {
+ return t.identifier(isAccessPolicy ? 'securityContext' : 'SECURITY_CONTEXT');
+ }
+
+ private static isShadowedByFunctionParam(name: string, path: NodePath): boolean {
+ let current: NodePath | null = path.parentPath;
+ while (current) {
+ const { node } = current;
+ if (
+ (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) &&
+ node.params.some(p => t.isIdentifier(p) && p.name === name)
+ ) {
+ return true;
}
+ current = current.parentPath;
}
+ return false;
}
- protected static transformUserAttributesMemberExpression(path: NodePath) {
- // Check if this is userAttributes.someProperty (object should be identifier named 'userAttributes')
- const fullPath = this.fullPath(path);
+ protected static transformCubeCloudShorthandIdentifier(path: NodePath, identifiers: string[], isAccessPolicy: boolean, resolveSymbol: SymbolResolver) {
+ if (!this.CUBE_CLOUD_SHORTHAND_IDENTIFIERS.includes(path.node.name)) {
+ return;
+ }
+ if (resolveSymbol(path.node.name)) {
+ return;
+ }
if (
- (t.isIdentifier(path.node.object, { name: 'userAttributes' }) || t.isIdentifier(path.node.object, { name: 'user_attributes' })) &&
- (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))
+ path.parent &&
+ (path.parent.type === 'MemberExpression' || path.parent.type === 'OptionalMemberExpression') &&
+ path.key === 'property'
) {
- // Replace userAttributes with securityContext.cubeCloud.userAttributes
- const securityContext = t.identifier('securityContext');
- const cubeCloud = t.memberExpression(securityContext, t.identifier('cubeCloud'));
- const userAttributes = t.memberExpression(cubeCloud, t.identifier('userAttributes'));
- const newMemberExpression = t.memberExpression(userAttributes, path.node.property, path.node.computed);
+ return;
+ }
+ if (this.isShadowedByFunctionParam(path.node.name, path)) {
+ return;
+ }
+ const contextId = this.securityContextIdentifier(isAccessPolicy);
+ const cubeCloud = t.memberExpression(contextId, t.identifier('cubeCloud'));
+ const prop = path.node.name === 'user_attributes' ? 'userAttributes' : path.node.name;
+ const newExpr = t.memberExpression(cubeCloud, t.identifier(prop));
+ path.replaceWith(newExpr);
+ identifiers.push(contextId.name);
+ }
+ protected static transformCubeCloudShorthandMemberExpression(path: NodePath, isAccessPolicy: boolean, resolveSymbol: SymbolResolver) {
+ if (
+ t.isIdentifier(path.node.object) &&
+ this.CUBE_CLOUD_SHORTHAND_IDENTIFIERS.includes(path.node.object.name) &&
+ !resolveSymbol(path.node.object.name) &&
+ !this.isShadowedByFunctionParam(path.node.object.name, path)
+ ) {
+ const contextId = this.securityContextIdentifier(isAccessPolicy);
+ const cubeCloud = t.memberExpression(contextId, t.identifier('cubeCloud'));
+ const prop = path.node.object.name === 'user_attributes' ? 'userAttributes' : path.node.object.name;
+ const shorthand = t.memberExpression(cubeCloud, t.identifier(prop));
+ const newMemberExpression = t.memberExpression(shorthand, path.node.property, path.node.computed);
path.replaceWith(newMemberExpression);
} else if (
- t.isMemberExpression(path.node.object) &&
+ (t.isMemberExpression(path.node.object) || t.isOptionalMemberExpression(path.node.object)) &&
t.isIdentifier(path.node.object.property, { name: 'user_attributes' }) &&
- !path.node.object.computed &&
- (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))
+ !path.node.object.computed
) {
- // Also handle case where user_attributes appears within a MemberExpression chain like
- // securityContext.cubeCloud.user_attributes
- // We need to convert user_attributes to userAttributes in such chains
const newObject = t.memberExpression(
path.node.object.object,
t.identifier('userAttributes'),
diff --git a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts
index 125de7ab3b016..b00945d95dc42 100644
--- a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts
+++ b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts
@@ -2683,4 +2683,375 @@ describe('Class unit tests', () => {
const re = new RegExp('(b__aid).*(b__bval_sum).*(b__count).*');
expect(re.test(sql[0])).toBeTruthy();
});
+
+ describe('SECURITY_CONTEXT unsafeValue with nested properties', () => {
+ // language=JavaScript
+ const securityContextCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM \${SECURITY_CONTEXT.cubeCloud.groups.unsafeValue() === 'admin' ? 'admin_orders' : 'public_orders'}\`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('should resolve nested unsafeValue to leaf value, not parent object', async () => {
+ await securityContextCompilers.compiler.compile();
+
+ const query = new PostgresQuery(securityContextCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: 'admin' } }
+ }
+ });
+ const [sql] = query.buildSqlAndParams();
+ expect(sql).toContain('admin_orders');
+ expect(sql).not.toContain('public_orders');
+ });
+
+ it('should resolve nested unsafeValue to non-matching leaf value', async () => {
+ await securityContextCompilers.compiler.compile();
+
+ const query = new PostgresQuery(securityContextCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: 'viewer' } }
+ }
+ });
+ const [sql] = query.buildSqlAndParams();
+ expect(sql).toContain('public_orders');
+ expect(sql).not.toContain('admin_orders');
+ });
+ });
+
+ describe('SECURITY_CONTEXT nested array filter with IN clause', () => {
+ // language=JavaScript
+ const securityContextArrayCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`
+ SELECT * FROM orders
+ WHERE \${SECURITY_CONTEXT.cubeCloud.groups.filter(groups => \`source IN (\${groups.join(',')})\`)}
+ \`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('should generate IN clause with params for nested array filter', async () => {
+ await securityContextArrayCompilers.compiler.compile();
+
+ const query = new PostgresQuery(securityContextArrayCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: ['admin', 'operator'] } }
+ }
+ });
+ const [sql, params] = query.buildSqlAndParams();
+ expect(sql).toContain('source IN (');
+ expect(sql).not.toContain('= ');
+ expect(params).toContain('admin');
+ expect(params).toContain('operator');
+ });
+
+ it('should generate 1=1 when nested array filter value is missing', async () => {
+ await securityContextArrayCompilers.compiler.compile();
+
+ const query = new PostgresQuery(securityContextArrayCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: {} }
+ }
+ });
+ const [sql] = query.buildSqlAndParams();
+ expect(sql).toContain('1 = 1');
+ });
+ });
+
+ describe('SECURITY_CONTEXT toString renders as param in interpolation', () => {
+ // language=JavaScript
+ const securityContextToStringCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM orders WHERE tenant_id = \${SECURITY_CONTEXT.cubeCloud.tenantId}\`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('should render nested primitive as param placeholder in SQL', async () => {
+ await securityContextToStringCompilers.compiler.compile();
+
+ const query = new PostgresQuery(securityContextToStringCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { tenantId: 'tenant_123' } }
+ }
+ });
+ const [sql, params] = query.buildSqlAndParams();
+ expect(sql).toMatch(/tenant_id = \$\d+/);
+ expect(params).toContain('tenant_123');
+ });
+ });
+
+ describe('SECURITY_CONTEXT with Tesseract (native SQL planner)', () => {
+ // language=JavaScript
+ const unsafeValueCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM \${SECURITY_CONTEXT.cubeCloud.groups.unsafeValue() === 'admin' ? 'admin_orders' : 'public_orders'}\`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('tesseract: unsafeValue resolves nested leaf value', async () => {
+ await unsafeValueCompilers.compiler.compile();
+
+ const query = new PostgresQuery(unsafeValueCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: 'admin' } }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql] = query.buildSqlAndParams();
+ expect(sql).toContain('admin_orders');
+ expect(sql).not.toContain('public_orders');
+ });
+
+ it('tesseract: unsafeValue resolves non-matching leaf value', async () => {
+ await unsafeValueCompilers.compiler.compile();
+
+ const query = new PostgresQuery(unsafeValueCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: 'viewer' } }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql] = query.buildSqlAndParams();
+ expect(sql).toContain('public_orders');
+ expect(sql).not.toContain('admin_orders');
+ });
+
+ // language=JavaScript
+ const arrayFilterCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`
+ SELECT * FROM orders
+ WHERE \${SECURITY_CONTEXT.cubeCloud.groups.filter(groups => \`source IN (\${groups.join(',')})\`)}
+ \`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('tesseract: array filter generates IN clause with params', async () => {
+ await arrayFilterCompilers.compiler.compile();
+
+ const query = new PostgresQuery(arrayFilterCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: ['admin', 'operator'] } }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql, params] = query.buildSqlAndParams();
+ expect(sql).toContain('source IN (');
+ expect(params).toContain('admin');
+ expect(params).toContain('operator');
+ });
+
+ it('tesseract: missing array filter value generates 1=1', async () => {
+ await arrayFilterCompilers.compiler.compile();
+
+ const query = new PostgresQuery(arrayFilterCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: {} }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql] = query.buildSqlAndParams();
+ expect(sql).toContain('1 = 1');
+ });
+
+ // language=JavaScript
+ const toStringCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM orders WHERE tenant_id = \${SECURITY_CONTEXT.cubeCloud.tenantId}\`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('tesseract: toString renders param placeholder in SQL', async () => {
+ await toStringCompilers.compiler.compile();
+
+ const query = new PostgresQuery(toStringCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { tenantId: 'tenant_123' } }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql, params] = query.buildSqlAndParams();
+ expect(sql).toMatch(/tenant_id = \$\d+/);
+ expect(params).toContain('tenant_123');
+ });
+
+ // language=JavaScript
+ const filterStringColumnCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM orders WHERE \${SECURITY_CONTEXT.cubeCloud.tenantId.filter('tenant_id')}\`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('tesseract: filter with string column and scalar value generates equality', async () => {
+ await filterStringColumnCompilers.compiler.compile();
+
+ const query = new PostgresQuery(filterStringColumnCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { tenantId: 'abc' } }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql, params] = query.buildSqlAndParams();
+ expect(sql).toMatch(/tenant_id = \$\d+/);
+ expect(params).toContain('abc');
+ });
+
+ // language=JavaScript
+ const filterStringColumnArrayCompilers = prepareJsCompiler(`
+ cube(\`orders\`, {
+ sql: \`SELECT * FROM orders WHERE \${SECURITY_CONTEXT.cubeCloud.groups.filter('source')}\`,
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primaryKey: true,
+ },
+ },
+ });
+ `);
+
+ it('tesseract: filter with string column and array value generates IN clause', async () => {
+ await filterStringColumnArrayCompilers.compiler.compile();
+
+ const query = new PostgresQuery(filterStringColumnArrayCompilers, {
+ measures: ['orders.count'],
+ timeDimensions: [],
+ contextSymbols: {
+ securityContext: { cubeCloud: { groups: ['admin', 'operator'] } }
+ },
+ useNativeSqlPlanner: true,
+ });
+ const [sql, params] = query.buildSqlAndParams();
+ expect(sql).toContain('source IN (');
+ expect(params).toContain('admin');
+ expect(params).toContain('operator');
+ });
+ });
});
diff --git a/packages/cubejs-schema-compiler/test/unit/context-symbols-proxy.test.ts b/packages/cubejs-schema-compiler/test/unit/context-symbols-proxy.test.ts
new file mode 100644
index 0000000000000..906b7a8666884
--- /dev/null
+++ b/packages/cubejs-schema-compiler/test/unit/context-symbols-proxy.test.ts
@@ -0,0 +1,123 @@
+import { CubeSymbols } from '../../src/compiler/CubeSymbols';
+
+describe('CubeSymbols.contextSymbolsProxyFrom', () => {
+ const allocateParam = (param: unknown) => `__param(${JSON.stringify(param)})`;
+
+ it('unsafeValue returns leaf primitive value, not parent object', () => {
+ const symbols = { cubeCloud: { groups: 'admin' } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.groups.unsafeValue()).toBe('admin');
+ });
+
+ it('unsafeValue returns leaf array value, not parent object', () => {
+ const symbols = { cubeCloud: { groups: ['admin', 'user'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.groups.unsafeValue()).toEqual(['admin', 'user']);
+ });
+
+ it('unsafeValue returns intermediate object at each level', () => {
+ const symbols = { cubeCloud: { groups: ['admin'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.unsafeValue()).toEqual({ groups: ['admin'] });
+ expect(proxy.unsafeValue()).toEqual(symbols);
+ });
+
+ it('unsafeValue returns undefined for missing properties', () => {
+ const symbols = { cubeCloud: { groups: ['admin'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.nonExistent.unsafeValue()).toBeUndefined();
+ });
+
+ it('unsafeValue returns numeric zero correctly', () => {
+ const symbols = { tenant: { id: 0 } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.tenant.id.unsafeValue()).toBe(0);
+ });
+
+ it('filter works on nested primitive values', () => {
+ const symbols = { cubeCloud: { tenantId: 'abc' } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.tenantId.filter('col')).toContain('col');
+ });
+
+ it('filter returns 1=1 for missing values', () => {
+ const symbols = { cubeCloud: {} };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.tenantId.filter('col')).toBe('1 = 1');
+ });
+
+ it('filter with nested array passes allocated params to function for IN clause', () => {
+ const symbols = { cubeCloud: { groups: ['admin', 'user'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = proxy.cubeCloud.groups.filter(
+ (groups) => `col IN (${groups.join(', ')})`
+ );
+ expect(result).toBe('col IN (__param("admin"), __param("user"))');
+ });
+
+ it('filter with nested array and string column produces IN clause', () => {
+ const symbols = { cubeCloud: { groups: ['admin', 'user'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = proxy.cubeCloud.groups.filter('col');
+ expect(result).toBe('col IN (__param("admin"), __param("user"))');
+ });
+
+ it('filter with nested primitive and string column produces equality', () => {
+ const symbols = { cubeCloud: { tenantId: 'abc' } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = proxy.cubeCloud.tenantId.filter('col');
+ expect(result).toBe('col = __param("abc")');
+ });
+
+ it('toString on nested primitive allocates param for interpolation', () => {
+ const symbols = { cubeCloud: { tenantId: 'abc' } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = `${proxy.cubeCloud.tenantId}`;
+ expect(result).toBe('__param("abc")');
+ });
+
+ it('toString on nested array allocates each element as param', () => {
+ const symbols = { cubeCloud: { groups: ['admin', 'user'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = `${proxy.cubeCloud.groups}`;
+ expect(result).toBe('__param("admin"),__param("user")');
+ });
+
+ it('toString on proxy wrapping array allocates params via toPrimitive', () => {
+ const symbols = { cubeCloud: { groups: ['admin', 'user'] } };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = String(proxy.cubeCloud.groups);
+ expect(result).toBe('__param("admin"),__param("user")');
+ });
+
+ it('toString on missing property returns empty string', () => {
+ const symbols = { cubeCloud: {} };
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ const result = `${proxy.cubeCloud.tenantId}`;
+ expect(result).toBe('');
+ });
+
+ it('deeply nested access on empty context chains without error', () => {
+ const symbols = {};
+ const proxy = CubeSymbols.contextSymbolsProxyFrom(symbols, allocateParam) as any;
+
+ expect(proxy.cubeCloud.tenantId.filter('col')).toBe('1 = 1');
+ expect(proxy.cubeCloud.tenantId.unsafeValue()).toBeUndefined();
+ expect(`${proxy.cubeCloud.tenantId}`).toBe('');
+ expect(proxy.a.b.c.d.e.filter('col')).toBe('1 = 1');
+ });
+});
diff --git a/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts b/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts
index aaf38133538f8..6493b6305757b 100644
--- a/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts
+++ b/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts
@@ -183,6 +183,201 @@ describe('Transpilers', () => {
expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.userAttributes.userId');
});
+ it('CubePropContextTranspiler with shorthand groups in values should transpile to securityContext.cubeCloud.groups', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: 'SELECT * FROM users',
+ dimensions: {
+ userId: {
+ sql: \`userId\`,
+ type: 'string'
+ }
+ },
+ accessPolicy: [
+ {
+ role: \`*\`,
+ rowLevel: {
+ filters: [
+ {
+ member: \`userId\`,
+ operator: \`equals\`,
+ values: [ groups ]
+ }
+ ]
+ }
+ }
+ ]
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values;
+ expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.groups');
+ });
+
+ it('CubePropContextTranspiler with bare shorthand groups (no array wrap) should transpile to securityContext.cubeCloud.groups', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: 'SELECT * FROM users',
+ dimensions: {
+ userId: {
+ sql: \`userId\`,
+ type: 'string'
+ }
+ },
+ accessPolicy: [
+ {
+ role: \`*\`,
+ rowLevel: {
+ filters: [
+ {
+ member: \`userId\`,
+ operator: \`equals\`,
+ values: groups
+ }
+ ]
+ }
+ }
+ ]
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values;
+ expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.groups');
+ });
+
+ it('CubePropContextTranspiler with shorthand groups member access should transpile to securityContext.cubeCloud.groups', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: 'SELECT * FROM users',
+ dimensions: {
+ userId: {
+ sql: \`userId\`,
+ type: 'string'
+ }
+ },
+ accessPolicy: [
+ {
+ role: \`*\`,
+ rowLevel: {
+ filters: [
+ {
+ member: \`userId\`,
+ operator: \`equals\`,
+ values: [ groups.someProperty ]
+ }
+ ]
+ }
+ }
+ ]
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values;
+ expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.groups.someProperty');
+ });
+
+ it('CubePropContextTranspiler with full path to groups should work normally', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: 'SELECT * FROM users',
+ dimensions: {
+ userId: {
+ sql: \`userId\`,
+ type: 'string'
+ }
+ },
+ accessPolicy: [
+ {
+ role: \`*\`,
+ rowLevel: {
+ filters: [
+ {
+ member: \`userId\`,
+ operator: \`equals\`,
+ values: [ securityContext.cubeCloud.groups ]
+ }
+ ]
+ }
+ }
+ ]
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values;
+ expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.groups');
+ });
+
+ it('CubePropContextTranspiler with groups shorthand in sql template should transpile to SECURITY_CONTEXT.cubeCloud.groups', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: \`SELECT * FROM users WHERE tenant_id = \${groups}\`,
+ dimensions: {
+ userId: {
+ sql: \`userId\`,
+ type: 'string'
+ }
+ }
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledSql = cubeEvaluator.cubeFromPath('Test').sql;
+ expect(transpiledSql!.toString()).toMatch('SECURITY_CONTEXT.cubeCloud.groups');
+ });
+
+ it('CubePropContextTranspiler with userAttributes shorthand in dimension sql should transpile to SECURITY_CONTEXT', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: 'SELECT * FROM users',
+ dimensions: {
+ userId: {
+ sql: \`\${userAttributes.region}\`,
+ type: 'string'
+ }
+ }
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledSql = cubeEvaluator.cubeFromPath('Test').dimensions.userId.sql;
+ expect(transpiledSql!.toString()).toMatch('SECURITY_CONTEXT.cubeCloud.userAttributes');
+ });
+
+ it('CubePropContextTranspiler should not transform groups shorthand when a cube member named groups exists', async () => {
+ const { cubeEvaluator, compiler } = prepareJsCompiler(`
+ cube(\`Test\`, {
+ sql: 'SELECT * FROM users',
+ dimensions: {
+ groups: {
+ sql: \`groups_col\`,
+ type: 'string'
+ },
+ filtered: {
+ sql: \`\${groups}\`,
+ type: 'string'
+ }
+ }
+ })
+ `);
+
+ await compiler.compile();
+
+ const transpiledSql = cubeEvaluator.cubeFromPath('Test').dimensions.filtered.sql;
+ expect(transpiledSql!.toString()).not.toMatch('SECURITY_CONTEXT');
+ expect(transpiledSql!.toString()).not.toMatch('securityContext');
+ expect(transpiledSql!.toString()).toMatch('groups');
+ });
+
it('ImportExportTranspiler', async () => {
const ieTranspiler = new ImportExportTranspiler();
const errorsReport = new ErrorReporter();
diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js b/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js
index 864fbafbbc2b9..4a94e5645707b 100644
--- a/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js
+++ b/packages/cubejs-testing/birdbox-fixtures/rbac/cube.js
@@ -186,6 +186,29 @@ module.exports = {
},
};
}
+ if (user === 'sc_test') {
+ if (password && password !== 'sc_test_password') {
+ throw new Error(`Password doesn't match for ${user}`);
+ }
+ return {
+ password,
+ superuser: false,
+ securityContext: {
+ cubeCloud: {
+ userAttributes: {
+ tenantId: '1',
+ },
+ groups: ['1', '2'],
+ },
+ auth: {
+ username: 'sc_test',
+ userAttributes: {},
+ roles: [],
+ groups: [],
+ },
+ },
+ };
+ }
throw new Error(`User "${user}" doesn't exist`);
}
};
diff --git a/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/security_context_test.js b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/security_context_test.js
new file mode 100644
index 0000000000000..30d3c5dfd37bb
--- /dev/null
+++ b/packages/cubejs-testing/birdbox-fixtures/rbac/model/cubes/security_context_test.js
@@ -0,0 +1,102 @@
+cube('security_context_test', {
+ sql: `
+ SELECT * FROM line_items
+ WHERE ${user_attributes.tenantId.filter('id')}
+ `,
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primary_key: true,
+ },
+ price_dim: {
+ sql: 'price',
+ type: 'number',
+ },
+ },
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ total_price: {
+ sql: 'price',
+ type: 'sum',
+ },
+ },
+});
+
+cube('sc_array_filter_test', {
+ sql: `
+ SELECT * FROM line_items
+ WHERE ${groups.filter('product_id')}
+ `,
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primary_key: true,
+ },
+ },
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+});
+
+cube('sc_interpolation_test', {
+ sql: `SELECT * FROM line_items WHERE id > ${user_attributes.tenantId}`,
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primary_key: true,
+ },
+ },
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+});
+
+cube('sc_groups_shorthand_test', {
+ sql_table: 'public.line_items',
+
+ dimensions: {
+ id: {
+ sql: 'id',
+ type: 'number',
+ primary_key: true,
+ },
+ product_id: {
+ sql: 'product_id',
+ type: 'number',
+ },
+ },
+
+ measures: {
+ count: {
+ type: 'count',
+ },
+ },
+
+ accessPolicy: [
+ {
+ role: '*',
+ rowLevel: {
+ filters: [{
+ member: 'product_id',
+ operator: 'equals',
+ values: groups,
+ }],
+ },
+ },
+ ],
+});
diff --git a/packages/cubejs-testing/test/smoke-rbac.test.ts b/packages/cubejs-testing/test/smoke-rbac.test.ts
index 22b584c2be4b7..494072a845659 100644
--- a/packages/cubejs-testing/test/smoke-rbac.test.ts
+++ b/packages/cubejs-testing/test/smoke-rbac.test.ts
@@ -782,6 +782,109 @@ describe('Cube RBAC Engine', () => {
});
});
+ describe('SECURITY_CONTEXT.cubeCloud features via SQL API', () => {
+ let connection: PgClient;
+
+ beforeAll(async () => {
+ connection = await createPostgresClient('sc_test', 'sc_test_password');
+ });
+
+ afterAll(async () => {
+ await connection.end();
+ }, JEST_AFTER_ALL_DEFAULT_TIMEOUT);
+
+ test('filter with scalar value generates equality (tenantId)', async () => {
+ const res = await connection.query(
+ 'SELECT * FROM security_context_test'
+ );
+ expect(res.rows.length).toBe(1);
+ });
+
+ test('filter with array value generates IN clause (groups)', async () => {
+ const res = await connection.query(
+ 'SELECT * FROM sc_array_filter_test'
+ );
+ expect(res.rows.length).toBeGreaterThan(0);
+ });
+
+ test('toString interpolation renders as param in SQL', async () => {
+ const res = await connection.query(
+ 'SELECT * FROM sc_interpolation_test'
+ );
+ expect(res.rows.length).toBeGreaterThan(0);
+ });
+
+ test('groups shorthand in access policy row filter', async () => {
+ const res = await connection.query(
+ 'SELECT * FROM sc_groups_shorthand_test'
+ );
+ expect(res.rows.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('SECURITY_CONTEXT.cubeCloud features via REST API', () => {
+ let scClient: CubeApi;
+
+ const SC_TEST_TOKEN = sign({
+ cubeCloud: {
+ userAttributes: {
+ tenantId: '1',
+ },
+ groups: ['1', '2'],
+ },
+ auth: {
+ username: 'sc_test',
+ userAttributes: {},
+ roles: [],
+ groups: [],
+ },
+ }, DEFAULT_CONFIG.CUBEJS_API_SECRET, {
+ expiresIn: '2 days'
+ });
+
+ beforeAll(async () => {
+ scClient = cubejs(async () => SC_TEST_TOKEN, {
+ apiUrl: birdbox.configuration.apiUrl,
+ });
+ });
+
+ test('filter with scalar value (tenantId) via REST', async () => {
+ const result = await scClient.load({
+ measures: ['security_context_test.count'],
+ });
+ const rows = result.rawData();
+ expect(rows.length).toBe(1);
+ expect(rows[0]['security_context_test.count']).toBeDefined();
+ });
+
+ test('filter with array value (groups) generates IN clause via REST', async () => {
+ const result = await scClient.load({
+ measures: ['sc_array_filter_test.count'],
+ });
+ const rows = result.rawData();
+ expect(rows.length).toBe(1);
+ expect(rows[0]['sc_array_filter_test.count']).toBeDefined();
+ });
+
+ test('toString interpolation via REST', async () => {
+ const result = await scClient.load({
+ measures: ['sc_interpolation_test.count'],
+ });
+ const rows = result.rawData();
+ expect(rows.length).toBe(1);
+ expect(rows[0]['sc_interpolation_test.count']).toBeDefined();
+ });
+
+ test('groups shorthand access policy via REST', async () => {
+ const result = await scClient.load({
+ measures: ['sc_groups_shorthand_test.count'],
+ });
+ const rows = result.rawData();
+ expect(rows.length).toBe(1);
+ expect(rows[0]['sc_groups_shorthand_test.count']).toBeDefined();
+ });
+ });
+
describe('RBAC via REST API', () => {
let client: CubeApi;
let defaultClient: CubeApi;
diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs
index d2d1bc523348a..d4338852e1cc9 100644
--- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs
+++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs
@@ -379,6 +379,14 @@ impl NativeMemberSql {
ParamValue::StringVec(prop_vec)
} else if let Ok(prop) = String::from_native(property_value.clone()) {
ParamValue::String(prop)
+ } else if let Ok(prop) = f64::from_native(property_value.clone()) {
+ if prop.fract() == 0.0 && prop.is_finite() {
+ ParamValue::String(format!("{}", prop as i64))
+ } else {
+ ParamValue::String(prop.to_string())
+ }
+ } else if let Ok(prop) = bool::from_native(property_value.clone()) {
+ ParamValue::String(prop.to_string())
} else if property_value.is_undefined()? || property_value.is_null()? {
ParamValue::None
} else {
@@ -418,15 +426,17 @@ impl NativeMemberSql {
.map(|v| Self::process_secutity_context_value(&proxy_state, &v))
.collect::, _>>()?;
- let values = values.to_native(context)?;
-
if let Ok(column) = column.to_function() {
- let result = column.call(vec![values])?;
+ let native_values = values.to_native(context)?;
+ let result = column.call(vec![native_values])?;
if let Ok(result) = result.to_string() {
result.value()?
} else {
"".to_string()
}
+ } else if let Ok(column) = column.to_string() {
+ let column_value = column.value()?;
+ format!("{} IN ({})", column_value, values.join(", "))
} else {
"".to_string()
}
@@ -460,6 +470,38 @@ impl NativeMemberSql {
Ok(NativeObjectHandle::new(result.into_object()))
}
+ fn security_context_to_string_fn(
+ context_holder: NativeContextHolder,
+ property_value: NativeObjectHandle,
+ proxy_state: ProxyStateWeak,
+ ) -> Result, CubeError> {
+ let str_value = if let Ok(prop_vec) = Vec::::from_native(property_value.clone()) {
+ Some(prop_vec)
+ } else if let Ok(prop) = String::from_native(property_value.clone()) {
+ Some(vec![prop])
+ } else if let Ok(prop) = f64::from_native(property_value.clone()) {
+ if prop.fract() == 0.0 && prop.is_finite() {
+ Some(vec![format!("{}", prop as i64)])
+ } else {
+ Some(vec![prop.to_string()])
+ }
+ } else if let Ok(prop) = bool::from_native(property_value.clone()) {
+ Some(vec![prop.to_string()])
+ } else {
+ None
+ };
+ let allocated = match str_value {
+ Some(values) => values
+ .iter()
+ .map(|v| Self::process_secutity_context_value(&proxy_state, v))
+ .collect::, _>>()?
+ .join(", "),
+ None => String::new(),
+ };
+ let result = context_holder.to_string_fn(allocated)?;
+ Ok(NativeObjectHandle::new(result.into_object()))
+ }
+
fn security_context_proxy(
context_holder: NativeContextHolder,
proxy_state: ProxyStateWeak,
@@ -484,8 +526,15 @@ impl NativeMemberSql {
}
if &prop == "unsafeValue" {
return Ok(Some(Self::security_context_unsafe_value_fn(
- inner_context,
+ inner_context.clone(),
+ target.clone(),
+ )?));
+ }
+ if &prop == "toString" || &prop == "valueOf" {
+ return Ok(Some(Self::security_context_to_string_fn(
+ inner_context.clone(),
target.clone(),
+ proxy_state.clone(),
)?));
}
let target_obj = target.to_struct()?;
@@ -497,32 +546,67 @@ impl NativeMemberSql {
property_value,
)?));
}
+ Ok(Some(Self::security_context_leaf_proxy(
+ inner_context,
+ proxy_state.clone(),
+ property_value,
+ )?))
+ })
+ }
- let result = inner_context.empty_struct()?;
- result.set_field(
- "filter",
- Self::security_context_filter_fn(
- inner_context.clone(),
- property_value.clone(),
- false,
- proxy_state.clone(),
- )?,
- )?;
- result.set_field(
- "requiredFilter",
- Self::security_context_filter_fn(
- inner_context.clone(),
- property_value.clone(),
- true,
- proxy_state.clone(),
- )?,
- )?;
- result.set_field(
- "unsafeValue",
- Self::security_context_unsafe_value_fn(inner_context, target.clone())?,
- )?;
- let result = NativeObjectHandle::new(result.into_object());
- Ok(Some(result))
+ /// Creates a chainable proxy for leaf (non-object) security context values.
+ /// The proxy target is a struct with method properties (filter, unsafeValue,
+ /// etc.). Unknown property access returns another chainable proxy, enabling
+ /// deeply nested paths like `SECURITY_CONTEXT.cubeCloud.tenantId.filter(...)`.
+ fn security_context_leaf_proxy(
+ context_holder: NativeContextHolder,
+ proxy_state: ProxyStateWeak,
+ property_value: NativeObjectHandle,
+ ) -> Result, CubeError> {
+ let result = context_holder.empty_struct()?;
+ result.set_field(
+ "filter",
+ Self::security_context_filter_fn(
+ context_holder.clone(),
+ property_value.clone(),
+ false,
+ proxy_state.clone(),
+ )?,
+ )?;
+ result.set_field(
+ "requiredFilter",
+ Self::security_context_filter_fn(
+ context_holder.clone(),
+ property_value.clone(),
+ true,
+ proxy_state.clone(),
+ )?,
+ )?;
+ result.set_field(
+ "unsafeValue",
+ Self::security_context_unsafe_value_fn(context_holder.clone(), property_value.clone())?,
+ )?;
+ result.set_field(
+ "toString",
+ Self::security_context_to_string_fn(
+ context_holder.clone(),
+ property_value.clone(),
+ proxy_state.clone(),
+ )?,
+ )?;
+ let methods_handle = NativeObjectHandle::new(result.into_object());
+ context_holder.make_proxy(Some(methods_handle), move |inner_context, target, prop| {
+ if let Ok(target_obj) = target.to_struct() {
+ if let Ok(true) = target_obj.has_field(&prop) {
+ return Ok(Some(target_obj.get_field(&prop)?));
+ }
+ }
+ let undef = inner_context.undefined()?;
+ Ok(Some(Self::security_context_leaf_proxy(
+ inner_context,
+ proxy_state.clone(),
+ undef,
+ )?))
})
}