Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ import { NexusToken } from './modules/nexus/entities/nexus-token.entity';
import { NexusBuild } from './modules/nexus/entities/nexus-build.entity';
import { UserGroupModule } from './modules/user-group/user-group.module';
import { UserGroup } from './modules/user-group/entities/user-group.entity';
import { RbacModule } from './modules/rbac/rbac.module';
import { Role } from './modules/rbac/entities/role.entity';
import { RolePermission } from './modules/rbac/entities/role-permission.entity';
import { UserRoleAssignment } from './modules/rbac/entities/user-role-assignment.entity';
import { UserRoleAssignmentDeviceGroup } from './modules/rbac/entities/user-role-assignment-device-group.entity';
import { ConsoleAudit } from './modules/rbac/entities/console-audit.entity';
import { RbacGuard } from './modules/rbac/guards/rbac.guard';

/**
* 应用根模块
Expand Down Expand Up @@ -112,6 +119,11 @@ import { UserGroup } from './modules/user-group/entities/user-group.entity';
NexusToken,
NexusBuild,
UserGroup,
Role,
RolePermission,
UserRoleAssignment,
UserRoleAssignmentDeviceGroup,
ConsoleAudit,
],
synchronize: true,
logging: false,
Expand All @@ -132,6 +144,7 @@ import { UserGroup } from './modules/user-group/entities/user-group.entity';
UpdateCheckModule,
NexusModule,
UserGroupModule,
RbacModule,
],
providers: [
{
Expand All @@ -142,6 +155,10 @@ import { UserGroup } from './modules/user-group/entities/user-group.entity';
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: RbacGuard,
},
],
})
export class AppModule {}
23 changes: 19 additions & 4 deletions src/common/guards/admin.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User, UserStatus } from '../../modules/user/entities/user.entity';

@Injectable()
/**
Expand All @@ -14,20 +16,33 @@ import {
* 只有管理员才能访问的路由会使用此守卫
*
* 验证逻辑:
* 检查用户信息中的isAdmin字段
* 读取数据库中的当前用户状态和 isAdmin 字段,不信任 JWT 内的旧权限状态
*/
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
constructor(private readonly dataSource: DataSource) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<{ user?: { isAdmin?: boolean } }>();
.getRequest<{ user?: { id?: string } }>();
const user = request.user;

if (!user) {
throw new ForbiddenException('请先登录');
}

if (!user.isAdmin) {
if (!user.id) {
throw new ForbiddenException('授权服务不可用');
}

const currentUser = await this.dataSource.getRepository(User).findOne({
where: { guid: user.id },
select: ['guid', 'isAdmin', 'status'],
});
const isAdmin =
currentUser?.isAdmin === true && currentUser.status === UserStatus.ACTIVE;

if (!isAdmin) {
throw new ForbiddenException('无权限访问,需要管理员权限');
}

Expand Down
17 changes: 9 additions & 8 deletions src/modules/address-book/address-book.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
Query,
HttpCode,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { AddressBookService } from './services';
import {
Expand All @@ -31,7 +30,7 @@ import {
} from './dto';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { AddressBookRuleService } from './services/address-book-rule.service';
import { AdminGuard } from '../../common/guards/admin.guard';
import { RequirePermission } from '../rbac/decorators/require-permission.decorator';

/**
* 地址簿控制器
Expand Down Expand Up @@ -240,6 +239,7 @@ export class AddressBookController {
}

@Get('shared/list')
@RequirePermission('address_books.view')
@HttpCode(HttpStatus.OK)
getWebSharedAddressBooks(
@Query() query: PaginationDto,
Expand All @@ -260,7 +260,7 @@ export class AddressBookController {
* @returns 操作结果
*/
@Post('shared/add')
@UseGuards(AdminGuard)
@RequirePermission('address_books.share')
@HttpCode(HttpStatus.OK)
async addSharedAddressBook(
@Body() dto: CreateAddressBookProfileDto,
Expand Down Expand Up @@ -288,7 +288,7 @@ export class AddressBookController {
* @returns 操作结果
*/
@Put('shared/update/profile')
@UseGuards(AdminGuard)
@RequirePermission('address_books.edit')
@HttpCode(HttpStatus.OK)
async updateSharedAddressBook(
@Body() dto: UpdateAddressBookProfileDto,
Expand Down Expand Up @@ -318,7 +318,7 @@ export class AddressBookController {
* @returns 操作结果
*/
@Delete('shared')
@UseGuards(AdminGuard)
@RequirePermission('address_books.edit')
@HttpCode(HttpStatus.OK)
async deleteSharedAddressBooks(
@Body() guids: string[],
Expand Down Expand Up @@ -573,6 +573,7 @@ export class AddressBookController {
* @returns 规则列表(分页)
*/
@Get('rules')
@RequirePermission('address_books.view')
@HttpCode(HttpStatus.OK)
async getRules(
@Query() query: RuleQueryDto,
Expand All @@ -590,7 +591,7 @@ export class AddressBookController {
* @returns 新创建的规则 GUID
*/
@Post('rule')
@UseGuards(AdminGuard)
@RequirePermission('address_books.share')
@HttpCode(HttpStatus.OK)
async addRule(@Body() dto: CreateRuleDto, @CurrentUser('id') userId: number) {
return this.ruleService.createRule(dto, String(userId));
Expand All @@ -605,7 +606,7 @@ export class AddressBookController {
* @returns 更新成功消息
*/
@Patch('rule')
@UseGuards(AdminGuard)
@RequirePermission('address_books.share')
@HttpCode(HttpStatus.OK)
async updateRule(
@Body() dto: UpdateRuleDto,
Expand All @@ -623,7 +624,7 @@ export class AddressBookController {
* @returns 删除成功消息
*/
@Delete('rules')
@UseGuards(AdminGuard)
@RequirePermission('address_books.share')
@HttpCode(HttpStatus.OK)
async deleteRules(
@Body() ruleGuids: string[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'reflect-metadata';
import { Repository } from 'typeorm';
import { AddressBookLegacyService } from './address-book-legacy.service';
import {
AddressBook,
AddressBookPeer,
AddressBookPeerTag,
AddressBookTag,
} from '../entities';
import { Sysinfo, Peer } from '../../../common/entities';

jest.mock('uuid', () => {
const cryptoModule =
jest.requireActual<typeof import('node:crypto')>('node:crypto');
return { v4: cryptoModule.randomUUID };
});

type MockRepository = {
findOne: jest.Mock;
find: jest.Mock;
delete: jest.Mock;
};

const repository = (): MockRepository => ({
findOne: jest.fn(),
find: jest.fn(),
delete: jest.fn().mockResolvedValue({ affected: 0 }),
});

describe('AddressBookLegacyService', () => {
it("does not delete peer tags belonging to another user's address book", async () => {
const addressBookRepository = repository();
const addressBookPeerRepository = repository();
const addressBookTagRepository = repository();
const addressBookPeerTagRepository = repository();
const sysinfoRepository = repository();
const peerRepository = repository();

addressBookRepository.findOne.mockResolvedValue({
guid: 'book-a',
owner: 'user-a',
isPersonal: true,
});
addressBookPeerRepository.find.mockResolvedValue([{ guid: 'entry-a' }]);

const service = new AddressBookLegacyService(
addressBookRepository as unknown as Repository<AddressBook>,
addressBookPeerRepository as unknown as Repository<AddressBookPeer>,
addressBookTagRepository as unknown as Repository<AddressBookTag>,
addressBookPeerTagRepository as unknown as Repository<AddressBookPeerTag>,
sysinfoRepository as unknown as Repository<Sysinfo>,
peerRepository as unknown as Repository<Peer>,
);

await service.updateLegacyAddressBook(
'user-a',
JSON.stringify({ tags: [], peers: [] }),
);

expect(addressBookPeerTagRepository.delete).toHaveBeenCalledTimes(1);
const criteria = addressBookPeerTagRepository.delete.mock.calls[0][0] as {
peerGuid: { value: string[] };
};
expect(criteria.peerGuid.value).toEqual(['entry-a']);
expect(addressBookPeerTagRepository.delete).not.toHaveBeenCalledWith({});
expect(addressBookTagRepository.delete).toHaveBeenCalledWith({
addressBookGuid: 'book-a',
});
expect(addressBookPeerRepository.delete).toHaveBeenCalledWith({
addressBookGuid: 'book-a',
});
});
});
13 changes: 11 additions & 2 deletions src/modules/address-book/services/address-book-legacy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,17 @@ export class AddressBookLegacyService {
}
}

// 删除所有现有标签和设备
await this.addressBookPeerTagRepository.delete({});
// Remove only this address book's peer-tag links. The legacy endpoint is
// user-scoped; an empty delete criteria would erase every user's tags.
const existingPeers = await this.addressBookPeerRepository.find({
where: { addressBookGuid },
select: ['guid'],
});
if (existingPeers.length > 0) {
await this.addressBookPeerTagRepository.delete({
peerGuid: In(existingPeers.map((peer) => peer.guid)),
});
}
await this.addressBookTagRepository.delete({ addressBookGuid });
await this.addressBookPeerRepository.delete({ addressBookGuid });

Expand Down
24 changes: 13 additions & 11 deletions src/modules/audit/audit.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Controller,
Post,
Body,
UseGuards,
Get,
Query,
Patch,
Expand All @@ -18,7 +17,10 @@ import {
import { FileAuditDto } from './dto/file-audit.dto';
import { AlarmAuditDto } from './dto/alarm-audit.dto';
import { Public } from '../auth/decorators/public.decorator';
import { AdminGuard } from '../../common/guards/admin.guard';
import {
RequirePermission,
RequireSuperAdmin,
} from '../rbac/decorators/require-permission.decorator';

/**
* 审计控制器
Expand Down Expand Up @@ -146,7 +148,7 @@ export class AuditsController {
* - 支持按连接类型过滤(type,-1表示未建立连接)
*
* 安全措施:
* - 使用AdminGuard进行认证
* - 需要 audit.view 权限
* - 只有管理员可以查询审计记录
*
* @param deviceId 被控端设备ID(模糊匹配)
Expand All @@ -157,7 +159,7 @@ export class AuditsController {
* @param current 当前页码
* @returns 连接审计列表
*/
@UseGuards(AdminGuard)
@RequirePermission('audit.view')
@Get('conn')
async queryConnectionAudits(
@Query('deviceId') deviceId?: string,
Expand All @@ -184,7 +186,7 @@ export class AuditsController {
* @param id 连接审计记录主键
* @param dto 更新数据
*/
@UseGuards(AdminGuard)
@RequireSuperAdmin()
@Patch('conn/:id')
async updateConnectionAudit(
@Param('id', ParseIntPipe) id: number,
Expand All @@ -209,7 +211,7 @@ export class AuditsController {
* - 支持按文件传输类型过滤(type: 0-发送, 1-接收)
*
* 安全措施:
* - 使用AdminGuard进行认证
* - 需要 audit.view 权限
* - 只有管理员可以查询审计记录
*
* @param deviceId 被控端设备ID(模糊匹配)
Expand All @@ -220,7 +222,7 @@ export class AuditsController {
* @param current 当前页码
* @returns 文件审计列表
*/
@UseGuards(AdminGuard)
@RequirePermission('audit.view')
@Get('file')
async queryFileAudits(
@Query('deviceId') deviceId?: string,
Expand Down Expand Up @@ -251,7 +253,7 @@ export class AuditsController {
* - 支持按告警类型过滤(type: 0-IP白名单, 1-超30次尝试, 2-1分钟6次尝试, 6-IPv6前缀超限, 7-终端OS登录backoff, 8-终端OS登录并发超限)
*
* 安全措施:
* - 使用AdminGuard进行认证
* - 需要 audit.view 权限
* - 只有管理员可以查询审计记录
*
* @param deviceId 被控端设备ID(模糊匹配)
Expand All @@ -262,7 +264,7 @@ export class AuditsController {
* @param current 当前页码
* @returns 告警审计列表
*/
@UseGuards(AdminGuard)
@RequirePermission('audit.view')
@Get('alarm')
async queryAlarmAudits(
@Query('deviceId') deviceId?: string,
Expand Down Expand Up @@ -292,7 +294,7 @@ export class AuditsController {
* - 支持按创建时间过滤
*
* 安全措施:
* - 使用AdminGuard进行认证
* - 需要 audit.view 权限
* - 只有管理员可以查询审计记录
*
* @param operator 操作人(模糊匹配)
Expand All @@ -301,7 +303,7 @@ export class AuditsController {
* @param created_at 创建时间(UTC时间字符串)
* @returns 控制台审计列表
*/
@UseGuards(AdminGuard)
@RequirePermission('audit.view')
@Get('console')
queryConsoleAudits(
@Query('operator') operator?: string,
Expand Down
2 changes: 2 additions & 0 deletions src/modules/audit/audit.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ConnectionAudit } from './entities/connection-audit.entity';
import { FileAudit } from './entities/file-audit.entity';
import { AlarmAudit } from './entities/alarm-audit.entity';
import { SettingsModule } from '../settings/settings.module';
import { RbacModule } from '../rbac/rbac.module';

/**
* 审计模块
Expand All @@ -27,6 +28,7 @@ import { SettingsModule } from '../settings/settings.module';
imports: [
TypeOrmModule.forFeature([ConnectionAudit, FileAudit, AlarmAudit]),
SettingsModule,
RbacModule,
],
controllers: [AuditController, AuditsController],
providers: [AuditService, AuditCleanupService],
Expand Down
Loading