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
19 changes: 0 additions & 19 deletions docs-mintlify/admin/users-and-permissions/index.mdx

This file was deleted.

119 changes: 119 additions & 0 deletions docs-mintlify/admin/users-and-permissions/manage-users.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Info>

Only users with the Admin role can manage other users.

</Info>

## 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.

<Info>

Admin roles are billed at the developer rate.

</Info>

## 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**.

<Warning>

You cannot deactivate yourself or the last active Admin user.

</Warning>

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**.

<Warning>

You cannot delete your own account. Deleting a user is irreversible.

</Warning>

## 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
2 changes: 1 addition & 1 deletion docs-mintlify/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
96 changes: 70 additions & 26 deletions packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> = {
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);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<t.Identifier>, 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);
}
});

Expand All @@ -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<t.MemberExpression>) {
// Check if this is userAttributes.someProperty (object should be identifier named 'userAttributes')
const fullPath = this.fullPath(path);
protected static transformCubeCloudShorthandIdentifier(path: NodePath<t.Identifier>, 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<t.MemberExpression>, 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'),
Expand Down
Loading
Loading