Access Control - Permissions, Roles, and Multitenancy

Getting started

When an app is created, three essential entities will automatically be generated with the necessary fields: permissions, users, and roles.

To enable multitenancy, it must be selected from the dropdown menu during app creation.

Upon selecting the Multitenancy option, the organization's (tenant) entity is automatically added. Subsequently, all entities, except roles and permissions, will have the "Organization" property since roles and permissions are not tenant-related. Additionally, with AI, the tenant’s name can be changed by specifying it in the app description.

Permissions

Default permissions

When the app is developed, four types of permissions will automatically be added for each entity by default: CREATE_(ENTITY), UPDATE_(ENTITY), READ_(ENTITY), and DELETE_(ENTITY).

Existing permissions can be managed and new ones can be created in the permissions section found in the left sidebar.

Custom permissions

When individual permissions need to be assigned to a user rather than through a role, this can be accomplished using the "Custom permissions" field in the user’s edit page.

These “Custom permissions” will be combined with the existing role permissions of the users.

If a new entity will be added after the initial release, associated permissions need to be manually added and assigned to the appropriate role.

Permissions on the Front End are administered through the hasPermission(currentUser: user, permissions: permission[]) function.

This function is utilized in various files, including frontend/src/components/AsideMenuList.tsx and frontend/src/pages/dashboard.tsx, among others.

The function is defined in the frontend/src/helpers/userPermissions.ts file.

The implementation details and usage of custom permissions are incorporated within the hasPermission() function.

export function hasPermission(user, permission_name: string | string[]) {
  if (!user?.app_role?.name) return false;  if (!permission_name) {
    return true;
  }
  const permissions = new Set<string>([
    ...(user?.custom_permissions ?? []).map((p) => p.name),
    ...(user?.app_role_permissions ?? []).map((p) => p.name),
  ]);
  if (typeof permission_name === 'string') {
    return permissions.has(permission_name) || user.app_role.globalAccess;
  } else {
    return permission_name.some((permission) => permissions.has(permission));
  }
}

On the backend, a middleware named check-permissions located at backend/src/middlewares/check-permissions.js is used to verify whether the specific operation permissions align with the current user’s permissions.

Roles

By default, three distinct roles - Super Administrator (with multitenancy), Administrator, and User - will be incorporated, each with an appropriate set of permissions. Additionally, with AI, role names can be changed by specifying it in the app description.

Existing roles can be managed and new ones created within the Roles section accessible from the left sidebar. Global access can be allocated to any role on the role’s edit page. This implies that users assigned a role with global access will have access to all features.

Essentially, on the frontend, permissions are derived from the current user’s role. If the user holds the "Super Administrator" role, the hasPermission function will consistently return true. Otherwise, permissions will be verified for specific operations, such as READ_USERS.

On the backend, roles are utilized to determine a user’s list of permissions.

Users

By default, four users will be created: one assigned the “Super Administrator” role, one assigned the “Administrator” role, and two assigned the “User” role.

Existing users can be managed and new ones created in the Users section, accessible from the left sidebar. Upon loading dashboard, a GET request will be sent to the /auth/me endpoint to retrieve the current user’s information. The returned data will then be stored in the Redux store. Redux Toolkit facilitates the management and utilization of objects within the Redux store. The currentUser object, containing all necessary properties such as roles and permissions, can be accessed and used from the Redux store.

Authentication & Authorization

Upon logging in, a GET request will be dispatched to the /auth/signin/local endpoint using the provided credentials. If the login is successful, a JWT token will be returned. This token will then be used to authorize the current user with Bearer Authorization.

In the backend, we handle JWT token verification within the index file (backend/src/index.js) using the Passport.js library (https://www.passportjs.org/). Middleware defined in backend/src/auth/auth.js is employed to extract the email from the token, locate the user by that email, and attach the user data (role and permissions) to the current request for subsequent use.

Multitenancy

Multitenancy typically involves querying the database to retrieve data that exclusively belongs to the current user. On backend endpoints, user data and roles are accessible via the “Auth” middleware (backend/src/auth/auth.js). This middleware attaches current user to request object and enables us to determine if a user possesses global access. If the user has global access, all data is retrieved; otherwise, the tenant ID is included in the filter within the findAll() and findAllAutocomplete() functions located in the routes directory (backend/src/routes) for all entities except permissions and roles.

Example: backend/src/routes/users.js

router.get(
'/',
wrapAsync(async (req, res) => {
  const filetype = req.query.filetype;
  const globalAccess = req.currentUser.app_role.globalAccess;
  const payload = await UsersDBApi.findAll(req.query, globalAccess);
  res.status(200).send(payload);
});