From b1b6e2df52e56d6b1046d035219122590eb58e7b Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 26 Oct 2026 11:28:40 -0500 Subject: [PATCH] fix(connections): authorize all agents affected by project profile bindings Project profiles can affect any company agent running work in that project. Check the existing configuration authority for all affected agents on bind, unbind, and retained profile edits, alongside project visibility. Co-Authored-By: Paperclip --- .../connection-management.integration.test.ts | 2 +- server/src/routes/connection-management-authz.ts | 2 +- server/src/services/connection-management.ts | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/connection-management.integration.test.ts b/server/src/__tests__/connection-management.integration.test.ts index 78140ec31d6..963940c179d 100644 --- a/server/src/__tests__/connection-management.integration.test.ts +++ b/server/src/__tests__/connection-management.integration.test.ts @@ -295,7 +295,7 @@ describe("connection management with production default agents", () => { const ownBinding = await f.request("POST", `${path}/bind`, { targetType: "issue", targetId: f.issueId }); expect(ownBinding, JSON.stringify(ownBinding)).toMatchObject({ status: 201 }); expect(await f.request("POST", `${path}/unbind`, { targetType: "issue", targetId: f.issueId })).toMatchObject({ status: 200 }); - for (const target of [{ targetType: "issue", targetId: issue.id }, { targetType: "routine", targetId: routine.id }, { targetType: "gateway", targetId: gateway.body.id }]) { + for (const target of [{ targetType: "project", targetId: f.projectId }, { targetType: "issue", targetId: issue.id }, { targetType: "routine", targetId: routine.id }, { targetType: "gateway", targetId: gateway.body.id }]) { expect(await f.request("POST", `${path}/bind`, target), JSON.stringify(target)).toMatchObject({ status: 403 }); const [saved] = await server.db.insert(toolProfileBindings).values({ ...target, companyId: f.companyId, profileId: profile.body.id }).returning(); expect(await f.request("PATCH", `/tool-profiles/${profile.body.id}`, { description: "Cannot edit retained target" })).toMatchObject({ status: 403 }); diff --git a/server/src/routes/connection-management-authz.ts b/server/src/routes/connection-management-authz.ts index 0e589266c6f..251d7bb8261 100644 --- a/server/src/routes/connection-management-authz.ts +++ b/server/src/routes/connection-management-authz.ts @@ -353,7 +353,7 @@ export function connectionManagementGuard(db: Db, purpose: "tool" | "chat" | "ai selfServiceUserId: connection.createdByUserId, }); } - if (["issue", "routine", "gateway"].includes(String(selected.targetType)) && text(selected.targetId)) bindingTargets.push({ targetType: String(selected.targetType), targetId: uuid(selected.targetId) }); + if (["project", "issue", "routine", "gateway"].includes(String(selected.targetType)) && text(selected.targetId)) bindingTargets.push({ targetType: String(selected.targetType), targetId: uuid(selected.targetId) }); if (text(selected.routineId)) extraResources.push({ ...resource, routineId: uuid(selected.routineId) }); if (selected.targetType === "project" && text(selected.targetId)) extraResources.push({ ...resource, projectId: String(selected.targetId) }); if (text(selected.projectId)) extraResources.push({ ...resource, projectId: String(selected.projectId) }); diff --git a/server/src/services/connection-management.ts b/server/src/services/connection-management.ts index 299cf897e13..4de417d2605 100644 --- a/server/src/services/connection-management.ts +++ b/server/src/services/connection-management.ts @@ -97,12 +97,10 @@ export function connectionManagementService(db: Db) { const companyAgents = await db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, resource.companyId)); companyAgents.forEach(agent => expandedTargets.add(agent.id)); } - if (["issue", "routine", "gateway"].includes(binding.targetType)) { + if (["project", "issue", "routine", "gateway"].includes(binding.targetType)) { await authorizeBindingTarget(actor, action, resource, binding, options); } - if (binding.targetType === "project") { - await authorize(actor, action, { ...resource, projectId: binding.targetId }, { ...options, expandedProfile: true, gatewayContextChecked: options.gatewayContextChecked || Boolean(resource.gatewayId) }); - } + } const linkedGateways = await db.select({ id: toolMcpGateways.id }).from(toolMcpGateways).where(and(eq(toolMcpGateways.companyId, resource.companyId), eq(toolMcpGateways.profileId, profile.id))); for (const gateway of linkedGateways) await authorizeBindingTarget(actor, action, resource, { targetType: "gateway", targetId: gateway.id }, options); @@ -250,7 +248,12 @@ export function connectionManagementService(db: Db) { options: NonNullable[3]> = {}): Promise { let resolved = { ...resource }; const checkOptions = { ...options, expandedProfile: true }; - if (binding.targetType === "issue") resolved.issueId = binding.targetId; + if (binding.targetType === "project") { + resolved.projectId = binding.targetId; + const rows = await db.select({ id: agents.id }).from(agents).where(eq(agents.companyId, resource.companyId)); + checkOptions.targetAgentIds = [...new Set([...(options.targetAgentIds ?? []), ...rows.map(row => row.id)])]; + } + else if (binding.targetType === "issue") resolved.issueId = binding.targetId; else if (binding.targetType === "routine") resolved.routineId = binding.targetId; else if (binding.targetType === "gateway") { const [gateway] = await db.select().from(toolMcpGateways).where(and(eq(toolMcpGateways.id, binding.targetId), eq(toolMcpGateways.companyId, resource.companyId)));