Appearance
UserManagerModule
The UserManagerModule
is designed to provide a global UserManager
instance, which is responsible for resolving users and their permissions in the context of a given request. This module is particularly useful when working with authentication and authorization, such as in the JwtAuthGuard
.
In the JwtAuthGuard
, the UserManager
instance is used to resolve a user based on their unique identifier (uid
). Once the user is resolved, their information, including permissions, is set in the request context to be used throughout the application's authorization process.
By utilizing the UserManagerModule
and UserManager
instance, you can efficiently handle user authentication, manage their roles and permissions, and ensure proper access control within your NestJS application.
Key Components
The module provides a set of reusable components that can be easily integrated into a NestJS project:
UserManager
UserManager is a core component in NestJS applications that handles user authentication. It streamlines access control by resolving users and their permissions, ensuring proper authorization for various resources and actions within the application.
How to install UserManagerModule to my app
To create a UserManagerModule
in NestJS, you can follow these steps:
Install the required dependencies for
UserManagerModule
:Create a
user-manager
folder in your project'ssrc/modules
directory.Inside the
user-manager
folder, create auser-manager.class.ts
file and extendsBaseUserManager
from NestKit with the following code:ts// src/modules/user-manager/user-manager.class.ts import { BaseUserManager } from '@deeepvision/nest-kit/dist/modules/user-manager'; import { Injectable } from '@nestjs/common'; import { commonAuthorizedUsersPermissions, commonUsersPermissions } from '@/permissions'; import { User } from '../users/user.entity'; @Injectable() export class UserManager extends BaseUserManager<User> { constructor( ) { super( [], commonUsersPermissions, commonAuthorizedUsersPermissions, ); } }
Inside the
user-manager
folder, create auser-manager.module.ts
file with the following code:tsimport { USER_MANAGER } from '@deeepvision/nest-kit'; import { Global, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ServiceToken } from '../service-accounts/service-tokens/service-token.entity'; import { UserToRole } from '../user-to-roles/user-to-role.entity'; import { User } from '../users/user.entity'; import { UsersModule } from '../users/users.module'; import { UserManager } from './user-manager.class'; @Global() @Module({ imports: [ TypeOrmModule.forFeature([User, UserToRole, ServiceToken]), UsersModule, ], providers: [ UserManager, { provide: USER_MANAGER, useExisting: UserManager, }, ], exports: [ UserManager, { provide: USER_MANAGER, useExisting: UserManager, }, ], }) export class UserManagerModule {}
Finally, import the
UserManagerModule
into your main AppModule or another module:ts// src/modules/app.module.ts import { Module } from '@nestjs/common'; import { UserManagerModule } from './modules/user-manager/user-manager.module'; @Module({ imports: [UserManagerModule], }) export class AppModule {}
In this code, we are importing the
UserManagerModule
and adding it to theimports
array of ourAppModule
.
How to inject UserManager
In order to inject the UserManager instance into your AuthGuard
class (for example), you'll need to use the UserManager
class. Here's an example:
ts
import { UserManager } from '@/modules/user-manager/user-manager.class.ts';
/**
* Guard that checks if user is authenticated
* Validate access token and resolve current user
*/
@Injectable()
export class JwtAuthGuard implements CanActivate {
private readonly logger = this.loggerFactory.create({
scope: JwtAuthGuard.name,
});
constructor(
private readonly userManager: UserManager,
// Other injections...
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// verify access token and extract uid
const user = await this.userManager.resolve(uid);
this.setUserInContext(context, user);
return true;
}
}
NestKit features JwtAuthGuard and GraphQLJwtAuthGuard that work seamlessly with UserManager
to handle user authentication and authorization. These guards are responsible for validating JWT tokens, resolving users along with their associated permissions, and injecting user data into the request context.
How to get the current user
For each GraphQL or REST request, you can use JwtAuthGuard (GraphQLJwtAuthGuard). This guard uses UserManager to resolve the user who made the request.
ts
@Resolver(() => Book)
@UseGuards(GraphQLJwtAuthGuard, GraphQLPermissionsGuard)
export class BooksResolver {
...
With the ActionContext decorator, you can obtain the context with the current user who made the request using an access token.
ts
@Resolver(() => Book)
@UseGuards(GraphQLJwtAuthGuard, GraphQLPermissionsGuard)
export class BooksResolver {
@Mutation(() => Book)
createBook(
@Args('input') input: CreateBookInput,
@ActionContext() ctx: ActionContext,
) {
// ctx.user <= current user info
...
}
}
...
Additionally, if you want to obtain information about the current user on the frontend, you can use the me
query. Use the access token to make this request.
Here's an example GraphQL query to retrieve the current user using the me query.
How UserManager
resolves user permissions
The UserManager
resolves user permissions by taking into account both custom permissions (User.permissions
) and roles (UserToRole
) associated with a user. Each role has its own set of permission groups and permissions.
When resolving permissions, UserManager
returns an array of ReadonlyResolvedPermission
objects, which are saved to the user's resolvedPermissions
property. This array represents the complete set of permissions a user has, including those inherited from their roles and any custom permissions directly assigned to the user.
ts
class UserManager {
// ...
async resolve(userId: string): Promise<User> {
// fetch user from database
user.resolvedPermissions = await this.resolveUserPermissions(user);
return user;
}
}
The ReadonlyResolvedPermission
interface has two properties:
id
: A unique identifier for the permission.scopes
: An array of permission scopes, which can be strings or arrays of strings, representing the specific access levels granted by the permission.
The UserManager
resolves user permissions by following these steps:
1. Fetch all active user roles from the database
ts
protected resolveUserPermissions = async (user: U): Promise<ReadonlyResolvedPermission[]> => {
/**
* Fetch all active user roles from the database
*
* This query retrieves all active user roles along with their associated
* organizations, permissions, and permission groups.
*/
const userToRoles = await this.userToRoleRepository
.createQueryBuilder('ur')
.select('ur.id', 'id')
.addSelect('ur.organization_id', 'organization_id')
.addSelect('o.group_id', 'organization_group_id')
.addSelect('o.modules', 'organization_modules')
.addSelect('r.permissions', 'role_permissions')
.addSelect('r.permission_groups', 'role_permission_groups')
.where(`
ur.user_id = :userId
AND ur.grant_at < now()
AND (ur.expire_at IS NULL OR ur.expire_at > now())
AND (o is null OR o.status = 'ACTIVE')
`, {
userId: user.id,
})
.leftJoin('ur.organization', 'o')
.leftJoin('ur.role', 'r')
.getRawMany<{
id: string;
organization_id: string | null;
organization_group_id: string | null;
organization_modules: string[];
role_permissions: string[];
role_permission_groups: string[];
}>();
This query retrieves all active user roles along with their associated organizations, permissions, and permission groups.
2. Merge common permissions (applicable to all users) and user-specific custom permissions
ts
protected resolveUserPermissions = async (user: U): Promise<ReadonlyResolvedPermission[]> => {
// 1. Fetch all active user roles from the database.
// ...
// 2. Merge common permissions (applicable to all users) and user-specific custom permissions.
let allResolvedPermissions: ResolvedPermission[] = mergeResolvedPermissions(
resolvePermissions(user.permissions),
resolvePermissions(this.commonPermissions),
);
3. Extract permissions from permission groups and merge them with role permissions
ts
protected resolveUserPermissions = async (user: U): Promise<ReadonlyResolvedPermission[]> => {
// 1. Fetch all active user roles from the database.
// 2. Merge common permissions (applicable to all users) and user-specific custom permissions.
for (const ur of userToRoles) {
// 3. Extract permissions from permission groups and merge them with role permissions.
const rps = mergeResolvedPermissions(
resolvePermissions(ur.role_permissions),
this.extractPermissionsFromGroups(ur.role_permission_groups),
);
// ...
}
4. Replace abstract org
scope with a specific organization scope in permissions
When a role is granted within an organization, the UserManager
replaces the abstract org scope in the permissions with a specific scope, such as org#hcorg:xxxxxxxxxx
. This ensures that the user's permissions are active and tied to a specific organization with a unique ID.
ts
protected resolveUserPermissions = async (user: U): Promise<ReadonlyResolvedPermission[]> => {
// 1. Fetch all active user roles from the database.
// ...
// 2. Merge common permissions (applicable to all users) and user-specific custom permissions.
// ...
for (const ur of userToRoles) {
// 3. Extract permissions from permission groups and merge them with role permissions.
if (ur.organization_id) {
/**
* Create a regular expression to match the available modules within the organization
*/
const regexp = new RegExp(`^${this.appConfig.shortname}:(${ur.organization_modules.join('|')}):`);
const permissionsWithResolvedOrgs = [];
// Iterate through the resolved permissions
for (const rp of rps) {
// Skip the permission if it doesn't match the organization modules
if (!regexp.test(rp.id)) {
continue;
}
// 4. Replace abstract `org` scope with a specific organization scope in permissions.
replaceOrgScopes(rp);
// Add the permission to the permissionsWithResolvedOrgs array
permissionsWithResolvedOrgs.push(rp);
}
// Merge role permissions with resolved scopes with all permissions
allResolvedPermissions = mergeResolvedPermissions(
allResolvedPermissions,
permissionsWithResolvedOrgs,
);
}
User access is restricted to only the available modules within the organization. This is implemented by filtering the permissions based on the available modules.
The replaceOrgScopes
function updates the scope from the generic org to the specific organization scope, ensuring that the user's permissions are correctly applied within the context of the organization they belong to. This allows for granular access control and organization-specific permissions.
ts
const replaceOrgScopes = (rp: ResolvedPermission) => {
/**
* Replace organization scopes
* @example
* hc:mam:brands[org]:update
* { id: 'js:mam:brands:update', scopes: [org#jsorg:xxxxx] }
*
**/
rp.scopes = replaceScope(rp.scopes, 'org', `org#${ur.organization_id}`);
return rp;
};
In order to provide user-specific access, the my scope is replaced by a specific scope in the format id#hcu:xxxxxxxxxxx
, where hcu:xxxxxxxxxxx
is the user's unique ID. This allows users to perform actions that are explicitly granted to them.
ts
allResolvedPermissions = allResolvedPermissions.map((rp) => {
rp.scopes = replaceScope(rp.scopes, 'my', `user#${user.id}`);
return rp;
});
return allResolvedPermissions;
How UserManager
extracts permissions from permission groups
The extractPermissionsFromGroups
method in UserManager
is designed to extract permissions associated with each permission group from the given Role.permissionGroups
array, taking into consideration the extends
property of the PermissionGroup
. When a permission group extends another group, the method extracts permissions from the extended group as well, recursively.
ts
export const permissionGroups: PermissionGroup[] = [
{
id: 'g:js:core:videos:read',
permissions: [
'js:core:videos:get',
'js:core:videos:list',
...
],
scopes: ['org', 'shared'],
},
{
id: 'g:js:core:videos:edit',
permissions: [
'js:core:videos:delete',
'js:core:videos:create',
...
],
extends: ['g:js:core:videos:read'],
scopes: ['org', 'shared'],
},
...
]
ts
role.permissionGroups = [
'g:js:core:videos[org]:edit',
...
]
ts
protected extractPermissionsFromGroups(idsWithScopes: string[]): ResolvedPermission[] {
// Initialize an empty `Set` to store the extracted permissions
// Recursive function to extract permissions from a permission group
const extractPermissions = (group: PermissionGroup, deep: number, scopes: (string | string[])) => {
// Extract permissions from the current permission group
// ...
// Merge the extracted permissions with the existing extracted permissions
// ...
// If the permission group extends another group, process the extended group recursively
if (group.extends) {
for (const gid of group.extends) {
const parent = this.permissionGroups.find((g) => g.id === gid);
if (parent) {
extractPermissions(parent, ++deep, scopes);
}
}
}
};
// Iterate through each permission group ID and scope
for (const idWithScopes of idsWithScopes) {
// Retrieve the permission group from the PermissionGroups data by its ID
const permissionGroup = this.permissionGroups.find(
(group) => group.id === idWithScopes
);
// If the permission group is found, extract its permissions
if (permissionGroup) {
extractGroupPermissions(permissionGroup);
}
}
// Return the extracted permissions array
return extractedPermissions;
}
How to create app service account context
The UserManager
provides a method called createAppServiceAccountContext
that allows your application to create a special context for cases where it needs to perform tasks internally without an external user request. This method generates a BaseServiceMethodContext
with a service account user, which can be used to call other services within the application.
Here's an example of how to call the createAppServiceAccountContext
method in your service:
First, import the
UserManager
and other required dependencies:tsimport { Injectable } from '@nestjs/common'; import { UserManager } from '@/modules/user-manager/user-manager.class.ts';
Then, inject the
UserManager
into your service:ts@Injectable() export class MyService { constructor(private readonly userManager: UserManager) {} }
Before you can use this feature, you need to create an app service account in the database and provide its ID
to the SERVICE_ACCOUNT_APP_ID
environment variable. Follow the steps below to create an app service account:
Ensure you have the
nestkit-cli
package installed globally. If you haven't installed it yet, run the following command:bashnpm i -g @deeepvision/nestkit-cli
In the root directory of your Nest project, run the following command to create an app service account in the database:
bashnestkit db seed app-sa
After running the command, the CLI will output the generated service account's
ID
. Copy thisID
and set it as the value for theSERVICE_ACCOUNT_APP_ID
environment variable in your project's.env.yml
file or your preferred method of managing environment variables.yamlSERVICE_ACCOUNT_APP_ID: <your_app_service_account_id>
Now, your application can use the createAppServiceAccountContext
method to generate a ServiceMethodContext
with the app service account user for internal tasks.
Create a method in your service that calls the createAppServiceAccountContext
method:
ts
async performTaskWithServiceAccountContext() {
const ctx = await this.userManager.createAppServiceAccountContext();
// Use the ctx to call other services that require context
// e.g., myService.doTask(opts, ctx);
}