2
0
Эх сурвалжийг харах

修复多租户地址模块跨租户访问错误处理

- 修复共享CRUD库中getById方法的执行顺序,确保租户验证先于数据权限验证
- 修复测试数据中的租户ID设置问题
- 添加专门的租户隔离集成测试
- 验证多租户数据隔离机制正常工作

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 сар өмнө
parent
commit
f92adfef9d

+ 10 - 4
packages/delivery-address-module-mt/src/routes/admin-custom.routes.mt.ts

@@ -122,11 +122,13 @@ const app = new OpenAPIHono<AuthContext>()
   .openapi(createDeliveryAddressRoute, async (c) => {
     try {
       const data = c.req.valid('json');
+      const user = c.get('user');
+      const tenantId = user?.tenantId || 1;
       const areaService = new AreaServiceMt(AppDataSource);
       const deliveryAddressService = new DeliveryAddressServiceMt(AppDataSource, areaService);
 
       // 使用包含地区验证的创建方法
-      const result = await deliveryAddressService.createWithValidation(data, 1); // TODO: 从上下文中获取租户ID
+      const result = await deliveryAddressService.createWithValidation(data, tenantId);
 
       return c.json(await parseWithAwait(AdminDeliveryAddressSchema, result), 201);
     } catch (error) {
@@ -156,11 +158,13 @@ const app = new OpenAPIHono<AuthContext>()
     try {
       const { id } = c.req.valid('param');
       const data = c.req.valid('json');
+      const user = c.get('user');
+      const tenantId = user?.tenantId || 1;
       const areaService = new AreaServiceMt(AppDataSource);
       const deliveryAddressService = new DeliveryAddressServiceMt(AppDataSource, areaService);
 
       // 使用包含地区验证的更新方法
-      const result = await deliveryAddressService.updateWithValidation(id, data, 1); // TODO: 从上下文中获取租户ID
+      const result = await deliveryAddressService.updateWithValidation(id, data, tenantId);
 
       if (!result) {
         return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -193,11 +197,13 @@ const app = new OpenAPIHono<AuthContext>()
   .openapi(deleteDeliveryAddressRoute, async (c) => {
     try {
       const { id } = c.req.valid('param');
+      const user = c.get('user');
+      const tenantId = user?.tenantId || 1;
       const areaService = new AreaServiceMt(AppDataSource);
       const deliveryAddressService = new DeliveryAddressServiceMt(AppDataSource, areaService);
 
-      // 使用通用CRUD服务的删除方法
-      const success = await deliveryAddressService.delete(id);
+      // 使用包含租户检查的删除方法
+      const success = await deliveryAddressService.deleteWithTenantCheck(id, tenantId);
 
       if (!success) {
         return c.json({ code: 404, message: '资源不存在' }, 404);

+ 4 - 0
packages/delivery-address-module-mt/src/routes/admin-routes.mt.ts

@@ -23,6 +23,10 @@ const adminCrudRoutes = createCrudRoutes({
     createdByField: 'createdBy',
     updatedByField: 'updatedBy'
   },
+  tenantOptions: {
+    enabled: true,
+    tenantIdField: 'tenantId'
+  },
   readOnly: true // 创建/更新/删除使用自定义路由
   // 注意:管理员路由不配置 dataPermission,保持完整CRUD功能
 });

+ 14 - 4
packages/delivery-address-module-mt/src/routes/user-custom.routes.mt.ts

@@ -166,8 +166,13 @@ const app = new OpenAPIHono<AuthContext>()
 
       console.debug('用户更新地址 - 用户ID:', user.id, '地址ID:', id);
 
-      // 先检查地址是否存在且属于当前用户
-      const existingAddress = await deliveryAddressService.repository.findOne({ where: { id } });
+      // 先检查地址是否存在且属于当前租户和用户
+      const existingAddress = await deliveryAddressService.repository.findOne({
+        where: {
+          id,
+          tenantId: user.tenantId
+        }
+      });
       console.debug('查询到的地址:', existingAddress);
       if (!existingAddress) {
         return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -220,8 +225,13 @@ const app = new OpenAPIHono<AuthContext>()
       const areaService = new AreaServiceMt(AppDataSource);
       const deliveryAddressService = new DeliveryAddressServiceMt(AppDataSource, areaService);
 
-      // 先检查地址是否存在且属于当前用户
-      const existingAddress = await deliveryAddressService.repository.findOne({ where: { id } });
+      // 先检查地址是否存在且属于当前租户和用户
+      const existingAddress = await deliveryAddressService.repository.findOne({
+        where: {
+          id,
+          tenantId: user.tenantId
+        }
+      });
       if (!existingAddress) {
         return c.json({ code: 404, message: '资源不存在' }, 404);
       }

+ 4 - 0
packages/delivery-address-module-mt/src/routes/user-routes.mt.ts

@@ -23,6 +23,10 @@ const userCrudRoutes = createCrudRoutes({
     createdByField: 'createdBy',
     updatedByField: 'updatedBy'
   },
+  tenantOptions: {
+    enabled: true,
+    tenantIdField: 'tenantId'
+  },
   readOnly: true, // 创建/更新/删除使用自定义路由
   // 配置数据权限控制,确保用户只能操作自己的数据
   dataPermission: {

+ 7 - 0
packages/delivery-address-module-mt/src/schemas/admin-delivery-address.mt.schema.ts

@@ -23,6 +23,13 @@ export const AdminDeliveryAddressSchema = z.object({
       description: '收货地址ID',
       example: 1
     }),
+  tenantId: z.number()
+    .int('租户ID必须是整数')
+    .positive('租户ID必须是正整数')
+    .openapi({
+      description: '租户ID',
+      example: 1
+    }),
   userId: z.number()
     .int('用户ID必须是整数')
     .positive('用户ID必须是正整数')

+ 41 - 1
packages/delivery-address-module-mt/src/services/delivery-address.mt.service.ts

@@ -179,6 +179,46 @@ export class DeliveryAddressServiceMt extends GenericCrudService<DeliveryAddress
       throw new Error('地区数据验证失败,请检查省市区信息是否正确');
     }
 
-    return this.update(id, data);
+    // 使用包含租户检查的更新方法
+    return this.updateWithTenantCheck(id, data, tenantId);
+  }
+
+  /**
+   * 更新配送地址(包含租户检查)
+   * @param id 地址ID
+   * @param data 更新数据
+   * @param tenantId 租户ID
+   * @returns 更新的地址
+   */
+  async updateWithTenantCheck(id: number, data: Partial<DeliveryAddressMt>, tenantId: number): Promise<DeliveryAddressMt | null> {
+    // 先检查地址是否存在且属于当前租户
+    const existingAddress = await this.repository.findOne({
+      where: {
+        id,
+        tenantId
+      }
+    });
+
+    if (!existingAddress) {
+      return null;
+    }
+
+    // 更新地址
+    Object.assign(existingAddress, data);
+    return await this.repository.save(existingAddress);
+  }
+
+  /**
+   * 删除配送地址(包含租户检查)
+   * @param id 地址ID
+   * @param tenantId 租户ID
+   * @returns 是否删除成功
+   */
+  async deleteWithTenantCheck(id: number, tenantId: number): Promise<boolean> {
+    const result = await this.repository.delete({
+      id,
+      tenantId
+    });
+    return result.affected !== 0;
   }
 }

+ 463 - 0
packages/delivery-address-module-mt/tests/integration/tenant-isolation.integration.test.ts

@@ -0,0 +1,463 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities, TestDataFactory } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { AreaEntityMt, AreaLevel } from '@d8d/geo-areas-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { userDeliveryAddressRoutesMt, adminDeliveryAddressRoutesMt } from '../../src/routes';
+import { DeliveryAddressMt } from '../../src/entities';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntityMt, RoleMt, AreaEntityMt, DeliveryAddressMt, FileMt])
+
+describe('多租户数据隔离测试', () => {
+  let userClient: ReturnType<typeof testClient<typeof userDeliveryAddressRoutesMt>>;
+  let adminClient: ReturnType<typeof testClient<typeof adminDeliveryAddressRoutesMt>>;
+
+  let tenant1UserToken: string;
+  let tenant2UserToken: string;
+  let tenant1AdminToken: string;
+  let tenant2AdminToken: string;
+
+  let tenant1User: UserEntityMt;
+  let tenant2User: UserEntityMt;
+  let tenant1Admin: UserEntityMt;
+  let tenant2Admin: UserEntityMt;
+
+  let tenant1Province: AreaEntityMt;
+  let tenant1City: AreaEntityMt;
+  let tenant1District: AreaEntityMt;
+  let tenant2Province: AreaEntityMt;
+  let tenant2City: AreaEntityMt;
+  let tenant2District: AreaEntityMt;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    userClient = testClient(userDeliveryAddressRoutesMt);
+    adminClient = testClient(adminDeliveryAddressRoutesMt);
+
+    // 创建租户1的测试数据
+    const tenant1Data = await TestDataFactory.createTestDataSet(1);
+    tenant1User = tenant1Data.user;
+    tenant1Province = tenant1Data.province;
+    tenant1City = tenant1Data.city;
+    tenant1District = tenant1Data.district;
+
+    // 创建租户2的测试数据
+    const tenant2Data = await TestDataFactory.createTestDataSet(2);
+    tenant2User = tenant2Data.user;
+    tenant2Province = tenant2Data.province;
+    tenant2City = tenant2Data.city;
+    tenant2District = tenant2Data.district;
+
+    // 创建租户1的管理员
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const userRepository = dataSource.getRepository(UserEntityMt);
+    tenant1Admin = userRepository.create({
+      username: `test_admin_tenant1_${Date.now()}`,
+      password: 'admin_password',
+      nickname: '租户1管理员',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(tenant1Admin);
+
+    // 创建租户2的管理员
+    tenant2Admin = userRepository.create({
+      username: `test_admin_tenant2_${Date.now()}`,
+      password: 'admin_password',
+      nickname: '租户2管理员',
+      registrationSource: 'web',
+      tenantId: 2
+    });
+    await userRepository.save(tenant2Admin);
+
+    // 生成各租户用户的token
+    tenant1UserToken = JWTUtil.generateToken({
+      id: tenant1User.id,
+      username: tenant1User.username,
+      roles: [{name:'user'}],
+      tenantId: 1
+    });
+
+    tenant2UserToken = JWTUtil.generateToken({
+      id: tenant2User.id,
+      username: tenant2User.username,
+      roles: [{name:'user'}],
+      tenantId: 2
+    });
+
+    // 生成各租户管理员的token
+    tenant1AdminToken = JWTUtil.generateToken({
+      id: tenant1Admin.id,
+      username: tenant1Admin.username,
+      roles: [{name:'admin'}],
+      tenantId: 1
+    });
+
+    tenant2AdminToken = JWTUtil.generateToken({
+      id: tenant2Admin.id,
+      username: tenant2Admin.username,
+      roles: [{name:'admin'}],
+      tenantId: 2
+    });
+
+    // 为两个租户都创建一些配送地址
+    await TestDataFactory.createTestDeliveryAddress(
+      tenant1User.id,
+      tenant1Province.id,
+      tenant1City.id,
+      tenant1District.id,
+      {
+        name: '租户1地址1',
+        phone: '13800138001',
+        address: '租户1地址1'
+      }
+    );
+
+    await TestDataFactory.createTestDeliveryAddress(
+      tenant2User.id,
+      tenant2Province.id,
+      tenant2City.id,
+      tenant2District.id,
+      {
+        name: '租户2地址1',
+        phone: '13800138002',
+        address: '租户2地址1'
+      }
+    );
+  });
+
+  describe('用户路由租户隔离', () => {
+    it('用户应该只能看到自己租户的地址', async () => {
+      // 租户1用户访问地址列表
+      const tenant1Response = await userClient.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1UserToken}`
+        }
+      });
+
+      expect(tenant1Response.status).toBe(200);
+      const tenant1Data = await tenant1Response.json();
+
+      if (tenant1Data && 'data' in tenant1Data) {
+        // 租户1用户应该只能看到租户1的地址
+        tenant1Data.data.forEach((address: any) => {
+          expect(address.tenantId).toBe(1);
+          expect(address.userId).toBe(tenant1User.id);
+        });
+      }
+
+      // 租户2用户访问地址列表
+      const tenant2Response = await userClient.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant2UserToken}`
+        }
+      });
+
+      expect(tenant2Response.status).toBe(200);
+      const tenant2Data = await tenant2Response.json();
+
+      if (tenant2Data && 'data' in tenant2Data) {
+        // 租户2用户应该只能看到租户2的地址
+        tenant2Data.data.forEach((address: any) => {
+          expect(address.tenantId).toBe(2);
+          expect(address.userId).toBe(tenant2User.id);
+        });
+      }
+    });
+
+    it('用户应该无法访问其他租户的地址详情', async () => {
+      // 为租户1和租户2都创建地址
+      const tenant1Address = await TestDataFactory.createTestDeliveryAddress(
+        tenant1User.id,
+        tenant1Province.id,
+        tenant1City.id,
+        tenant1District.id,
+        {
+          name: '租户1专用地址',
+          phone: '13800138003',
+          address: '租户1专用地址',
+          tenantId: 1
+        }
+      );
+
+      const tenant2Address = await TestDataFactory.createTestDeliveryAddress(
+        tenant2User.id,
+        tenant2Province.id,
+        tenant2City.id,
+        tenant2District.id,
+        {
+          name: '租户2专用地址',
+          phone: '13800138004',
+          address: '租户2专用地址',
+          tenantId: 2
+        }
+      );
+
+      // 租户1用户尝试访问租户2的地址
+      const crossTenantResponse = await userClient[':id'].$get({
+        param: { id: tenant2Address.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1UserToken}`
+        }
+      });
+
+      // 应该返回404,因为租户隔离机制应该阻止跨租户访问
+      expect(crossTenantResponse.status).toBe(404);
+
+      // 租户2用户尝试访问租户1的地址
+      const crossTenantResponse2 = await userClient[':id'].$get({
+        param: { id: tenant1Address.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant2UserToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(crossTenantResponse2.status).toBe(404);
+    });
+
+    it('用户应该无法操作其他租户的地址', async () => {
+      // 为租户2创建一个地址
+      const tenant2Address = await TestDataFactory.createTestDeliveryAddress(
+        tenant2User.id,
+        tenant2Province.id,
+        tenant2City.id,
+        tenant2District.id,
+        {
+          name: '租户2地址',
+          phone: '13800138005',
+          address: '租户2地址',
+          tenantId: 2
+        }
+      );
+
+      // 租户1用户尝试更新租户2的地址
+      const updateResponse = await userClient[':id'].$put({
+        param: { id: tenant2Address.id },
+        json: { name: '尝试更新' }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1UserToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(updateResponse.status).toBe(404);
+
+      // 租户1用户尝试删除租户2的地址
+      const deleteResponse = await userClient[':id'].$delete({
+        param: { id: tenant2Address.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1UserToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(deleteResponse.status).toBe(404);
+    });
+  });
+
+  describe('管理员路由租户隔离', () => {
+    it('管理员应该只能管理自己租户的地址', async () => {
+      // 租户1管理员访问地址列表
+      const tenant1Response = await adminClient.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1AdminToken}`
+        }
+      });
+
+      expect(tenant1Response.status).toBe(200);
+      const tenant1Data = await tenant1Response.json();
+
+      if (tenant1Data && 'data' in tenant1Data) {
+        // 租户1管理员应该只能看到租户1的地址
+        tenant1Data.data.forEach((address: any) => {
+          expect(address.tenantId).toBe(1);
+        });
+      }
+
+      // 租户2管理员访问地址列表
+      const tenant2Response = await adminClient.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant2AdminToken}`
+        }
+      });
+
+      expect(tenant2Response.status).toBe(200);
+      const tenant2Data = await tenant2Response.json();
+
+      if (tenant2Data && 'data' in tenant2Data) {
+        // 租户2管理员应该只能看到租户2的地址
+        tenant2Data.data.forEach((address: any) => {
+          expect(address.tenantId).toBe(2);
+        });
+      }
+    });
+
+    it('管理员应该无法访问其他租户的地址详情', async () => {
+      // 为租户2创建一个地址
+      const tenant2Address = await TestDataFactory.createTestDeliveryAddress(
+        tenant2User.id,
+        tenant2Province.id,
+        tenant2City.id,
+        tenant2District.id,
+        {
+          name: '租户2管理员测试地址',
+          phone: '13800138006',
+          address: '租户2管理员测试地址',
+          tenantId: 2
+        }
+      );
+
+      // 租户1管理员尝试访问租户2的地址
+      const crossTenantResponse = await adminClient[':id'].$get({
+        param: { id: tenant2Address.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1AdminToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(crossTenantResponse.status).toBe(404);
+    });
+
+    it('管理员应该无法操作其他租户的地址', async () => {
+      // 为租户2创建一个地址
+      const tenant2Address = await TestDataFactory.createTestDeliveryAddress(
+        tenant2User.id,
+        tenant2Province.id,
+        tenant2City.id,
+        tenant2District.id,
+        {
+          name: '租户2操作测试地址',
+          phone: '13800138007',
+          address: '租户2操作测试地址',
+          tenantId: 2
+        }
+      );
+
+      // 租户1管理员尝试更新租户2的地址
+      const updateResponse = await adminClient[':id'].$put({
+        param: { id: tenant2Address.id },
+        json: { name: '租户1尝试更新' }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1AdminToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(updateResponse.status).toBe(404);
+
+      // 租户1管理员尝试删除租户2的地址
+      const deleteResponse = await adminClient[':id'].$delete({
+        param: { id: tenant2Address.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1AdminToken}`
+        }
+      });
+
+      // 应该返回404
+      expect(deleteResponse.status).toBe(404);
+    });
+
+    it('管理员应该只能在自己租户内创建地址', async () => {
+      // 租户1管理员为租户1用户创建地址
+      const createData = {
+        userId: tenant1User.id,
+        name: '租户1管理员创建',
+        phone: '13800138008',
+        address: '租户1管理员创建',
+        receiverProvince: tenant1Province.id,
+        receiverCity: tenant1City.id,
+        receiverDistrict: tenant1District.id,
+        receiverTown: 1,
+        state: 1,
+        isDefault: 0
+      };
+
+      const response = await adminClient.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1AdminToken}`
+        }
+      });
+
+      expect(response.status).toBe(201);
+
+      // 验证创建的地址属于租户1
+      if (response.status === 201) {
+        const data = await response.json();
+        expect(data.tenantId).toBe(1);
+      }
+    });
+  });
+
+  describe('地区数据租户隔离', () => {
+    it('应该使用正确的租户地区数据', async () => {
+      // 租户1用户创建地址,应该使用租户1的地区数据
+      const createData = {
+        name: '租户1地区测试',
+        phone: '13800138009',
+        address: '租户1地区测试',
+        receiverProvince: tenant1Province.id,
+        receiverCity: tenant1City.id,
+        receiverDistrict: tenant1District.id,
+        receiverTown: 1,
+        state: 1,
+        isDefault: 0
+      };
+
+      const response = await userClient.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1UserToken}`
+        }
+      });
+
+      expect(response.status).toBe(201);
+
+      // 租户1用户尝试使用租户2的地区数据创建地址
+      const crossTenantAreaData = {
+        name: '跨租户地区测试',
+        phone: '13800138010',
+        address: '跨租户地区测试',
+        receiverProvince: tenant2Province.id, // 租户2的省份
+        receiverCity: tenant1City.id,
+        receiverDistrict: tenant1District.id,
+        receiverTown: 1,
+        state: 1,
+        isDefault: 0
+      };
+
+      const crossTenantResponse = await userClient.index.$post({
+        json: crossTenantAreaData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${tenant1UserToken}`
+        }
+      });
+
+      // 应该返回400,因为地区数据不属于当前租户
+      expect(crossTenantResponse.status).toBe(400);
+    });
+  });
+});

+ 12 - 8
packages/shared-crud/src/services/generic-crud.service.ts

@@ -191,16 +191,12 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
       relations
     });
 
-    // 数据权限验证
-    if (entity && this.dataPermissionOptions?.enabled && userId) {
-      const hasPermission = await this.checkPermission(entity, userId);
-      if (!hasPermission) {
-        throw new PermissionError('没有权限访问该资源');
-      }
+    if (!entity) {
+      return null;
     }
 
-    // 租户隔离验证
-    if (entity && this.tenantOptions?.enabled) {
+    // 租户隔离验证 - 先于数据权限验证
+    if (this.tenantOptions?.enabled) {
       const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
       const tenantId = await this.extractTenantId(userId);
       if (tenantId !== undefined && tenantId !== null) {
@@ -211,6 +207,14 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
       }
     }
 
+    // 数据权限验证
+    if (this.dataPermissionOptions?.enabled && userId) {
+      const hasPermission = await this.checkPermission(entity, userId);
+      if (!hasPermission) {
+        throw new PermissionError('没有权限访问该资源');
+      }
+    }
+
     return entity;
   }