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);
});