npm package: @lark-apaas/nestjs-authzpaas
Description: FullStack Nestjs authzpaas
Latest version: 0.1.8

# nestjs-authzpaas

`nestjs-authzpaas` 是一个基于 NestJS 的权限管理模块，提供了完整的 RBAC（基于角色的访问控制）和 ABAC（基于属性的访问控制）解决方案。它基于 [CASL](https://casl.js.org/) 实现，支持角色检查、权限检查、环境检查等多种鉴权方式。

## 目录

- [核心概念](#核心概念)
- [快速开始](#快速开始)
- [装饰器使用](#装饰器使用)
- [核心组件](#核心组件)
- [高级用法](#高级用法)
- [最佳实践](#最佳实践)
- [运行时权限 SDK（AuthorizationSDK）](#运行时权限-sdkauthorizationsdk)

## 核心概念

### 权限模型

本模块采用以下权限模型：

- **角色 (Role)**: 用户所属的角色，如 `admin`、`editor`、`viewer`

### 核心组件

| 组件 | 类型 | 说明 |
|------|------|------|
| `AuthZPaasGuard` | 全局守卫 | 自动拦截所有请求，执行鉴权检查 |
| `RolesMiddleware` | 中间件 | 自动获取并注入用户角色信息到请求上下文 |
| `CanRole` | 装饰器 | 声明式角色检查 |

## 快速开始

### 1. 安装依赖

```bash
npm install @lark-apaas/nestjs-authzpaas
# 或
yarn add @lark-apaas/nestjs-authzpaas
```

### 2. 配置模块

在 `app.module.ts` 中导入并配置 `AuthZPaasModule`：

```typescript
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
import { AuthZPaasModule, RolesMiddleware } from '@lark-apaas/nestjs-authzpaas';
import { UserContextMiddleware } from '@lark-apaas/fullstack-nestjs-core';

@Module({
  imports: [
    // 配置 AuthZPaas 模块
    AuthZPaasModule.forRoot({
      permissionApi: {
        timeout: 5000, // 权限 API 超时时间（毫秒）, 默认 5000
      },
    }),
  ],
  controllers: [/* ... */],
  providers: [/* ... */],
})
```

### 3. 异步配置（可选）

如果需要从 `ConfigService` 获取配置：

```typescript
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot(),
    AuthZPaasModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        permissionApi: {
          timeout: 5000,
        },
      }),
    }),
  ],
})
export class AppModule {}
```

## 配置选项详解

### 核心配置

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `permissionApi` | `PermissionApiConfig` | - | 权限 API 配置 |

## 装饰器使用

> 💡 **装饰器是最常用的权限控制方式**，推荐优先使用装饰器来进行权限检查，简单、直观、声明式。

### 1. CanRole - 角色检查

使用 `@CanRole()` 装饰器检查用户角色。

#### 示例 1: 单个角色

```typescript
import { Controller, Get } from '@nestjs/common';
import { CanRole } from '@lark-apaas/nestjs-authzpaas';

@Controller('demo')
export class DemoController {
  // 需要 admin 角色
  @CanRole(['admin'])
  @Get('admin-only')
  adminOnly() {
    return {
      message: '✅ 这是管理员专属接口',
      timestamp: new Date(),
    };
  }
}
```

#### 示例 2: 多个角色（OR 逻辑）

```typescript
@Controller('content')
export class ContentController {
  // 拥有 admin 或 editor 任一角色即可访问
  @CanRole(['admin', 'editor'])
  @Get('manage')
  manage() {
    return '管理内容';
  }
}
```

## 核心组件

> 📚 **核心组件是装饰器的底层实现**，了解核心组件有助于深入理解权限系统的工作原理。

### AuthZPaasGuard

`AuthZPaasGuard` 是一个全局守卫，会自动拦截所有请求并执行以下操作：

1. **提取用户ID**: 从 `req.userContext.userId` 中提取用户ID
2. **角色检查**: 如果使用了 `@CanRole()`，检查用户角色
5. **注入权限数据**: 将权限数据附加到 `req.userPermissions`

**工作流程**:

```yaml
   请求 
    ↓
AuthZPaasGuard
    ↓
提取 userId
    ↓
检查角色/权限
    ↓
注入权限数据
    ↓
Controller
```

**配置**: `AuthZPaasGuard` 会在模块导入时自动注册为全局守卫，无需手动配置。

### PermissionService

`PermissionService` 提供权限管理的核心功能。

#### 主要方法

```typescript
import { Injectable } from '@nestjs/common';
import { PermissionService } from '@lark-apaas/nestjs-authzpaas';

@Injectable()
export class YourService {
  constructor(private readonly permissionService: PermissionService) {}

  async checkUserPermission(userId: string) {
    // 1. 获取用户权限数据（带缓存）
    const permissionData = await this.permissionService.getUserPermissions(userId);
    console.log('用户角色:', permissionData.roles);
    console.log('用户权限:', permissionData.permissions);

    // 2. 获取用户的 CASL Ability 实例
    const ability = await this.permissionService.getAbility(userId);
    
    // 使用 Ability 检查权限
    const canReadUser = ability.can('read', 'User');
    const canCreateTask = ability.can('create', 'Task');
    const canManageAll = ability.can('manage', 'all');

    // 3. 检查权限要求
    const hasPermission = await this.permissionService.checkPermissions(
      [{ actions: ['read'], subject: 'User' }],
      userId
    );

    // 4. 清除用户缓存（权限变更时）
    this.permissionService.clearUserCache(userId);

    // 5. 清除所有缓存
    this.permissionService.clearAllCache();

    // 6. 获取缓存统计
    const stats = this.permissionService.getCacheStats();
    console.log('缓存统计:', stats);
  }
}
```

#### 工作原理

1. 从 `req.userContext.userId` 中提取用户ID
2. 调用 `PermissionService.getUserPermissions()` 获取权限数据
3. 将角色列表注入到 `req.userContext.userRoles`
4. 如果获取失败，记录警告但不阻塞请求（由 Guard 处理）

## 高级用法

### 1. 手动权限检查

在某些场景下，你可能需要在业务逻辑中手动检查权限，而不是使用装饰器：

```typescript
import { Injectable } from '@nestjs/common';
import { PermissionService } from '@lark-apaas/nestjs-authzpaas';

@Injectable()
export class TasksService {
  constructor(private readonly permissionService: PermissionService) {}

  async processTask(userId: string, taskId: string) {
    // 获取用户权限数据
    const permissionData = await this.permissionService.getUserPermissions(userId);
    
    // 检查权限
    const canUpdate = await this.permissionService.checkPermissions(
      [{ actions: ['update'], subject: 'Task' }],
      userId
    );

    if (!canUpdate) {
      throw new Error('没有权限');
    }

    // 执行业务逻辑
    return '任务处理成功';
  }
}
```

### 2. 使用 CASL Ability 进行细粒度控制

CASL Ability 提供了更灵活的权限检查方式：

```typescript
import { Injectable } from '@nestjs/common';
import { PermissionService, ROLE_SUBJECT } from '@lark-apaas/nestjs-authzpaas';

@Injectable()
export class ArticleService {
  constructor(private readonly permissionService: PermissionService) {}

  async canUserEditArticle(userId: string, articleId: string) {
    const ability = await this.permissionService.getAbility(userId);
    
    // 检查是否可以更新文章
    if (ability.can('update', 'Article')) {
      return true;
    }
    
    // 或者检查是否有 manage all 权限
    if (ability.can('manage', 'all')) {
      return true;
    }
    
    // 检查角色
    if (ability.can('admin', ROLE_SUBJECT)) {
      return true;
    }
    
    return false;
  }
}
```

## 权限 API 规范

### 请求格式

权限 API 的 endpoint 配置支持动态参数替换：

```typescript
AuthZPaasModule.forRoot({
  permissionApi: {
    timeout: 5000,
  },
})
```

### 响应格式

您的权限 API 必须返回以下格式的 JSON 数据：

```json
{
  "roles": ["role_admin", "role_editor"],
  "fetchedAt": "2024-01-01T00:00:00.000Z"
}
```

### 字段说明

- `userId`: 用户ID（必需）
- `roles`: 角色名称列表（必需）
  - 字符串数组 `['role_admin', 'role_editor']`
- `fetchedAt`: 获取时间（可选，SDK 会自动添加）

## 错误处理

### 1. 常见异常类型

| 异常 | 说明 | HTTP 状态码 |
|------|------|------------|
| `PermissionDeniedException.unauthenticated()` | 未认证（无 userId） | 403 |
| `PermissionDeniedException.roleRequired()` | 缺少必需的角色 | 403 |
| `PermissionDeniedException.permissionDenied()` | 缺少必需的权限 | 403 |
| `PermissionDeniedException.environmentRestricted()` | 环境限制（如 IP 白名单） | 403 |

## 最佳实践

### 1. 使用 TypeScript 类型提示

充分利用 TypeScript 的类型系统：

```typescript
import { 
  CanPermission, 
  CanRole,
  PermissionService,
  ROLE_SUBJECT 
} from '@lark-apaas/nestjs-authzpaas';

// TypeScript 会自动提供代码提示和类型检查
```

### 2. 测试建议

编写单元测试时，可以 mock `PermissionService`：

```typescript
describe('TasksController', () => {
  let controller: TasksController;
  let permissionService: PermissionService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [TasksController],
      providers: [
        {
          provide: PermissionService,
          useValue: {
            getUserPermissions: jest.fn(),
            getAbility: jest.fn(),
            checkPermissions: jest.fn(),
          },
        },
      ],
    }).compile();

    controller = module.get<TasksController>(TasksController);
    permissionService = module.get<PermissionService>(PermissionService);
  });

  it('should check permissions', async () => {
    jest.spyOn(permissionService, 'checkPermissions').mockResolvedValue(true);
    
    // 测试逻辑...
  });
});
```

## 运行时权限 SDK（AuthorizationSDK）

`AuthorizationSDK` 提供对平台运行态权限 API 的封装，包括角色管理、成员管理和混合搜索三个子模块。SDK 自动从请求上下文获取 `appId` 和 `userId`，无需手动传递。

### 注入方式

```typescript
import { Injectable } from '@nestjs/common';
import { AuthorizationSDK } from '@lark-apaas/nestjs-authzpaas';

@Injectable()
export class YourService {
  constructor(private readonly authzSDK: AuthorizationSDK) {}
}
```

`AuthorizationSDK` 暴露三个子对象：

| 属性 | 类型 | 说明 |
|------|------|------|
| `authzSDK.roles` | `RolesAPI` | 角色 CRUD |
| `authzSDK.members` | `MembersAPI` | 角色成员管理 |
| `authzSDK.search` | `SearchAPI` | 混合搜索（用户/部门/群组） |

---

### roles — 角色管理

#### `roles.list(params?)`

获取角色列表。

```typescript
const roles = await authzSDK.roles.list();
// 带参数
const roles = await authzSDK.roles.list({ needMember: true });
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `needMember` | `boolean` | 是否返回角色成员信息 |
| `userID` | `string \| number` | 可选，覆盖上下文中的 userId |

**返回**: `ForceRoleDTO[]`

#### `roles.get(bizID)`

获取单个角色详情。

```typescript
const role = await authzSDK.roles.get('admin');
```

**返回**: `ForceRoleDTO`

#### `roles.create(params)`

创建角色。

```typescript
const result = await authzSDK.roles.create({
  role: {
    bizID: 'editor',
    name: '编辑者',
    description: '可编辑内容',
  },
});
// result: { bizID: 'editor', apiID: '...' }
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `role.bizID` | `string` | 业务 ID（唯一标识） |
| `role.name` | `string` | 角色名称 |
| `role.description` | `string?` | 角色描述 |

**返回**: `CreateRoleResponse`（`{ bizID, apiID }`）

#### `roles.update(bizID, params)`

更新角色。

```typescript
await authzSDK.roles.update('editor', {
  role: { name: '高级编辑者', description: '更新后的描述' },
});
```

#### `roles.delete(bizID)`

删除角色。

```typescript
await authzSDK.roles.delete('editor');
```

---

### members — 成员管理

所有成员操作以角色的 `bizID` 为入口。成员通过 `RoleMemberDTO` 按类型分组：

```typescript
interface RoleMemberDTO {
  userList?: UserSimpleDTO[];       // 用户列表
  departmentList?: DepartmentDTO[]; // 部门列表
  groupChatList?: ChatSimpleDTO[];  // 群组列表
}
```

#### `members.list(bizID, params?)`

获取角色下的成员列表。不传 `type` 时返回所有类型的成员。

```typescript
// 获取所有类型成员
const res = await authzSDK.members.list('admin');

// 仅获取用户类型
const res = await authzSDK.members.list('admin', { type: 'User' });

// 分页
const res = await authzSDK.members.list('admin', { page: 1, page_size: 20 });
```

| 参数 | 类型 | 说明 |
|------|------|------|
| `type` | `MemberType?` | 成员类型过滤：`'User'` \| `'Department'` \| `'GroupChat'` |
| `page` | `number?` | 页码 |
| `page_size` | `number?` | 每页数量 |

**返回**: `ListMembersResponse`

```typescript
interface ListMembersResponse {
  members: RoleMemberDTO;
  total: number;
  hasMore: boolean;
}
```

#### `members.add(bizID, params)`

向角色添加成员。支持同时添加用户、部门、群组。

```typescript
// 添加用户
await authzSDK.members.add('admin', {
  members: { userList: [{ userID: '1826968659245100' }] },
});

// 添加部门
await authzSDK.members.add('admin', {
  members: { departmentList: [{ id: '7579138586559286811' }] },
});

// 添加群组
await authzSDK.members.add('admin', {
  members: { groupChatList: [{ chatID: '123456' }] },
});
```

#### `members.remove(bizID, params)`

从角色移除成员。SDK 会自动补充 `allEmployees`、`public`、`presetGroup` 等默认参数。

```typescript
await authzSDK.members.remove('admin', {
  members: { userList: [{ userID: '1826968659245100' }] },
});
```

#### `members.clear(bizID)`

清空角色下的所有成员。

```typescript
await authzSDK.members.clear('admin');
```

---

### search — 混合搜索

#### `search.search(params)`

搜索用户、部门、群组。不传 `filters` 时自动使用默认参数（每种类型 pageSize=20）。

```typescript
// 最简用法
const res = await authzSDK.search.search({ query: '张三' });

// 自定义过滤
const res = await authzSDK.search.search({
  query: '张三',
  filters: {
    userParam: { commonParam: { searchable: true, pageSize: 10, offset: 0 } },
    departmentParam: { commonParam: { searchable: true, pageSize: 5, offset: 0 } },
    chatParam: { commonParam: { searchable: true, pageSize: 5, offset: 0 } },
  },
  includeExternalUser: false,
  includeExternalGroup: true,
});
```

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `query` | `string` | — | 搜索关键词（必填） |
| `filters` | `FilterParams?` | 三种类型各 pageSize=20 | 按类型配置搜索参数 |
| `includeExternalUser` | `boolean?` | `false` | 是否包含外部用户 |
| `includeExternalGroup` | `boolean?` | `true` | 是否包含外部群组 |

**返回**: `SearchResponse`

```typescript
interface SearchResponse {
  result: SearchResult;
}

interface SearchResult {
  userResult?: { total?: number; items?: SearchUserEntity[] };
  departmentResult?: { total?: number; items?: DepartmentEntity[] };
  chatResult?: { total?: number; items?: SearchChatEntity[] };
}
```

---

### 完整使用示例

```typescript
import { Controller, Get, Post, Delete, Param, Body, Query } from '@nestjs/common';
import { AuthorizationSDK } from '@lark-apaas/nestjs-authzpaas';

@Controller('api/authorization')
export class AuthorizationController {
  constructor(private readonly authzSDK: AuthorizationSDK) {}

  @Get('roles')
  listRoles() {
    return this.authzSDK.roles.list();
  }

  @Post('roles')
  createRole(@Body() dto: { role: { bizID: string; name: string; description?: string } }) {
    return this.authzSDK.roles.create(dto);
  }

  @Get('roles/:bizID/members')
  listMembers(@Param('bizID') bizID: string, @Query('type') type?: string) {
    return this.authzSDK.members.list(bizID, { type: type as any });
  }

  @Post('roles/:bizID/members')
  addMembers(@Param('bizID') bizID: string, @Body() dto: { members: any }) {
    return this.authzSDK.members.add(bizID, dto);
  }

  @Post('search')
  search(@Body() dto: { query: string }) {
    return this.authzSDK.search.search(dto);
  }
}
```

## 完整示例

查看 `examples/node-demo` 目录获取完整的工作示例，包括：

- ✅ 模块配置和中间件设置
- ✅ Mock 权限 API 实现
- ✅ 角色和权限装饰器使用
- ✅ 手动权限检查
- ✅ CASL Ability 使用
- ✅ 测试脚本

运行示例：

```bash
cd examples/node-demo
yarn install
yarn start

# 测试不同用户的权限
curl -H "x-user-id: user-admin" http://localhost:3000/demo/admin-only
curl -H "x-user-id: user-normal" http://localhost:3000/demo/reader-only
```

## License

MIT

## 相关链接

- [CASL 文档](https://casl.js.org/)
- [NestJS 文档](https://docs.nestjs.com/)
- [示例项目](../../examples/node-demo)