Skip to content

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:

  1. Install the required dependencies for UserManagerModule:

  2. Create a user-manager folder in your project's src/modules directory.

  3. Inside the user-manager folder, create a user-manager.class.ts file and extends BaseUserManager 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,
        );
      }
    }
  4. Inside the user-manager folder, create a user-manager.module.ts file with the following code:

    ts
    import { 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 {}
  5. 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 the imports array of our AppModule.

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:

  1. First, import the UserManager and other required dependencies:

    ts
    import { Injectable } from '@nestjs/common';
    import { UserManager } from '@/modules/user-manager/user-manager.class.ts';
  2. 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:

  1. Ensure you have the nestkit-cli package installed globally. If you haven't installed it yet, run the following command:

    bash
    npm i -g @deeepvision/nestkit-cli
  2. In the root directory of your Nest project, run the following command to create an app service account in the database:

    bash
    nestkit db seed app-sa
  3. After running the command, the CLI will output the generated service account's ID. Copy this ID and set it as the value for the SERVICE_ACCOUNT_APP_ID environment variable in your project's .env.yml file or your preferred method of managing environment variables.

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

Created by DeepVision Software.