Activepieces 数据隔离规则解析:多租户安全下的 projectId / platformId 过滤与 ArrayContains 实践
2026/9/12 16:30:41 网站建设 项目流程

Activepieces 数据隔离规则解析:多租户安全下的 projectId / platformId 过滤与 ArrayContains 实践

【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces

导读

本文以仓库 .claude/rules/data-isolation.md 中定义的工程规范为核心,结合 Activepieces 服务端(packages/server/api)与共享类型定义(packages/core/shared)的真实源码,深入讲解两条多租户数据隔离铁律:所有数据库查询必须按projectIdplatformId过滤,以及跨项目共享资源必须使用 TypeORM 的ArrayContains([projectId])操作符查询projectIds数组列。读完本文,你将理解这两条规则背后的数据模型设计、在应用连接(App Connection)、事件目的地(Event Destination)等模块中的落地形态,以及如何在新增查询时避免跨租户数据泄漏。

一、规则总览:两条必须遵守的多租户安全基线

仓库中 .claude/rules/data-isolation.md 全文定义了 Activepieces 后端开发中数据隔离的最基本约束:

  1. ALL database queries MUST filter byprojectIdorplatformIdfor multi-tenant safety.所有数据库查询都必须按projectIdplatformId进行过滤,以保证多租户安全。
  2. For connections with multi-project access, useArrayContains([projectId])on theprojectIdsarray column.对于支持多项目访问的连接(connections),在projectIds数组列上使用ArrayContains([projectId])

这两条规则共同回答了一个核心问题:Activepieces 是典型的多租户(multi-tenant)SaaS 架构,平台(Platform)下挂多个项目(Project),项目内再承载流程、连接、模板、事件目的地等资源。任何一条 SQL 查询若缺少租户维度的 WHERE 条件,就可能把项目 A 的流程、密钥或运行记录暴露给项目 B,构成严重的数据越权。

  • 规则一是所有资源查询的通用底线:无论是findOneByupdatedelete还是createQueryBuilder,都必须携带projectIdplatformId作为过滤条件;
  • 规则二是规则一在"一连接多项目"场景下的具体落地方式:由于连接(App Connection)通过projectIds数组列与多个项目关联,无法用简单的等值比较(=)匹配,因此必须使用 PostgreSQL 数组包含操作符对应的 TypeORMArrayContains,判断目标项目 ID 是否落在数组内。

二、规则一的源码落地:从查询构建器到仓储层

规则一在服务端并非停留在规范文档层面,而是渗透在几乎所有业务仓储(repository)与服务的查询代码中。以下两个代表性模块可以直观印证。

2.1 事件目的地(Event Destination):平台维度的强制过滤

packages/server/api/src/app/event-destinations/event-destinations.service.ts 中,事件目的地的创建、更新、删除、列表查询全部以platformId为强制条件:

// 创建:实体写入时强制携带 platformId const entity: EventDestination = { id: apId(), created: new Date().toISOString(), updated: new Date().toISOString(), platformId, scope: EventDestinationScope.PLATFORM, events: request.events, url: request.url, } return eventDestinationRepo().save(entity) // 更新与删除:均以 { id, platformId } 复合条件定位,杜绝"只知道 id 就能改别人的资源" await eventDestinationRepo().update({ id, platformId }, request) await eventDestinationRepo().delete({ id, platformId }) // 列表:查询构建器同样以 platformId 收窄范围 const queryBuilder = eventDestinationRepo() .createQueryBuilder('event_destination') .where({ platformId })

注意更新与删除操作把platformIdid一起作为FindOptionsWhere条件:即便调用方传入了一个属于其他平台的 ID,复合条件也无法命中任何行,这是规则一在写操作上的典型应用。

更进一步,事件触发的查询还演示了规则一与规则二的组合使用(见下文第三部分)。

2.2 流程版本(Flow Version):项目维度的等值过滤

在 packages/server/api/src/app/app-connection/app-connection-service/app-connection.handler.ts 中,统计引用某连接的项目内流程数量时,使用createQueryBuilder同时约束项目与发布版本:

const query = flowVersionRepo() .createQueryBuilder('flow_version') .innerJoin('flow', 'flow', 'flow.id = flow_version."flowId"') .where('flow."projectId" = :projectId', { projectId }) .andWhere('flow_version.id = flow."publishedVersionId"') .andWhere('flow_version."connectionIds" && :externalIds', { externalIds: [externalId] })

这里flow."projectId" = :projectId就是规则一最直接的形式——通过项目 ID 的等值过滤把统计范围严格锁定在单个项目内。

三、规则二的源码落地:projectIds数组列与ArrayContains

规则二针对的是 Activepieces 中一类特殊资源:应用连接(App Connection)。在 packages/core/shared/src/lib/automation/app-connection/app-connection.ts 中可以清晰看到这类资源的数据模型设计:

export enum AppConnectionScope { PROJECT = 'PROJECT', PLATFORM = 'PLATFORM', } // AppConnection 实体核心字段(节选) { externalId: string type: Type scope: AppConnectionScope pieceName: string displayName: string projectIds: string[] // ← 关键:连接可同时归属于多个项目 platformId: string status: AppConnectionStatus }

从类型定义(app-connection.ts 中的 zod schema)可以看到projectIds: z.array(ApId),即该字段是一个项目 ID 数组。这意味着一个连接可能同时被多个项目共享——典型场景是平台级(PLATFORM作用域)连接或跨项目复用的 OAuth2 凭据。

由于列是数组而非标量,WHERE projectIds = :projectId这类等值查询永远无法命中,必须借助 PostgreSQL 的数组包含语义。这正是ArrayContains的用途。

3.1 查找:projectIds: ArrayContains([projectId])

在 app-connection-service.ts 中,upsert判断"同作用域同 externalId 的连接是否已存在"时:

const existingConnection = await appConnectionsRepo().findOneBy({ externalId, scope, platformId, ...(projectIds ? { projectIds: ArrayContains(projectIds) } : {}), })

而在 app-connection.handler.ts 中,连接刷新与校验同样严格遵循该模式:

// lockAndRefreshConnection:按项目维度定位连接 const encryptedAppConnection = await appConnectionsRepo().findOneBy({ projectIds: ArrayContains([projectId]), externalId, }) // revalidateConnection:同时携带 id / platformId / projectIds 三重条件 const encryptedAppConnection = await appConnectionsRepo().findOneBy({ id, platformId, projectIds: ArrayContains([projectId]), })

可以看到,即使已经用id精确定位,代码仍然叠加platformIdprojectIds: ArrayContains([projectId]),把多租户过滤做成了"条件三件套"——这正是规则一与规则二在同一查询中的协同体现。

3.2 数组列在"一对多"查询中的其他用法

规则二使用ArrayContains判断"数组包含某值"(@>语义);在 template.service.ts 中还可以看到同族操作符ArrayOverlap&&语义,判断两个数组是否有交集)的用法:

if (pieces) { commonFilters.pieces = ArrayOverlap(pieces) } if (category) { commonFilters.categories = ArrayContains([category]) }

这说明 Activepieces 在数组列上同时使用了包含(ArrayContains)与重叠(ArrayOverlap)两类操作符:规则二强调的场景(一个项目 ID 是否属于连接的projectIds数组)用ArrayContains([projectId]),而模板按多个 piece 过滤的场景则用ArrayOverlap。二者都要求查询条件与列数据类型一致,且都依赖 PostgreSQL 数组类型的原生支持。

四、跨模块联动:一条查询中的规则一 + 规则二组合

事件目的地模块的触发逻辑(event-destinations.service.ts)是两条规则协同工作的完整示例:

const conditions: FindOptionsWhere<EventDestinationSchema>[] = [{ platformId, // 规则一:平台维度 events: ArrayContains([event.action]), // 规则二:数组列包含 scope: EventDestinationScope.PLATFORM, }] const broadcastToProject = !isNil(projectId) && PROJECT_SCOPE_EVENTS.includes(event.action) if (broadcastToProject) { conditions.push({ platformId, // 规则一:平台维度 projectId, // 规则一:项目维度(等值) events: ArrayContains([event.action]), // 规则二:数组列包含 scope: EventDestinationScope.PROJECT, }) }

这段代码清晰地展示了过滤条件的组织方式:

  • 平台级目的地:只按platformId过滤(规则一)即可,因为作用域为PLATFORM
  • 项目级目的地:必须同时按platformIdprojectId过滤(规则一的完整形态),保证事件只会派发到归属项目自己的目的地;
  • 事件类型匹配events同样是数组列,因此使用ArrayContains([event.action])判断事件是否在订阅清单中(规则二)。

五、为什么必须是ArrayContains:底层语义与常见误区

从 TypeORM 与 PostgreSQL 的对应关系看:

TypeORM 操作符PostgreSQL 操作符语义适用场景
Equal=等值比较标量列(如platformIdprojectId
ArrayContains(arr)@>左数组包含右数组全部元素projectIds数组列包含指定项目 ID
ArrayOverlap(arr)&&两数组存在交集按多个值过滤数组列

因此:

  • 查询"这个连接属于哪个项目"时,projectIds: ArrayContains([projectId])是唯一正确的写法;
  • 直接把projectIdprojectIds做等值比较属于常见误区,会因类型不匹配或语义错误导致查询失效,进而可能退化为"不过滤"或异常,直接违背多租户安全底线;
  • 反向操作符(如数组被包含)虽然在 PostgreSQL 中存在(<@),但 Activepieces 规范明确要求以ArrayContains([projectId])的正向写法统一表达"项目属于连接"这一语义,便于代码审查者一眼识别过滤意图。

六、实践清单:新增查询时的自检项

结合规则文档与上述源码,在 Activepieces 服务端新增或修改查询时,建议按以下清单自检:

  1. 租户维度是否齐全:查询是否携带了platformIdprojectId过滤?写操作(update/delete)是否把租户条件与主键一起放入FindOptionsWhere
  2. 列类型是否匹配:目标列是标量(platformIdprojectId)还是数组(projectIdseventsconnectionIds)?数组列必须使用ArrayContains(包含)或ArrayOverlap(交集),不能用等值比较。
  3. 共享资源是否走数组语义:连接、事件目的地等多项目/多事件资源,是否按规范使用ArrayContains([projectId])/ArrayContains([event.action])
  4. 复合条件是否防越权:即使已按id定位,是否仍叠加租户条件(参考 app-connection.handler.ts 的"条件三件套")?
  5. 查询构建器与仓储 API 是否一致createQueryBuilder中的.where('flow."projectId" = :projectId')repo.findOneBy({ projectIds: ArrayContains([projectId]) })都遵循同一套租户过滤原则,不应在重构时丢失。

结语

.claude/rules/data-isolation.md 用两句话定义了 Activepieces 后端多租户安全的全部底线,而其威力来自源码中的系统性落地:无论是 app-connection-service.ts 的 upsert 幂等判断、app-connection.handler.ts 的连接刷新与校验,还是 event-destinations.service.ts 的事件派发,都能看到platformId/projectId过滤与ArrayContains数组包含查询的组合。理解并遵守这两条规则,是保障多租户隔离、避免跨项目数据泄漏的前提;将它们作为代码审查的强制项,则是把安全从"规范"变成"事实"的关键。

【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询