From 20534f871b3a5c987ea194497d6f2dac1559890a Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 9 Oct 2026 09:13:32 -0500 Subject: [PATCH] fix(slack): keep grant revocation enforced during install callbacks Co-Authored-By: Paperclip --- server/src/services/connectors/slack/setup/service.ts | 2 +- ui/storybook/fixtures/SlackSetupFixture.tsx | 10 +++++++--- ui/storybook/stories/slack-managed-setup.stories.tsx | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/services/connectors/slack/setup/service.ts b/server/src/services/connectors/slack/setup/service.ts index a6ac43dd350..8a5b2ee67bc 100644 --- a/server/src/services/connectors/slack/setup/service.ts +++ b/server/src/services/connectors/slack/setup/service.ts @@ -116,7 +116,7 @@ export function slackChatRegistrationService(db: Db, options: SlackSetupOptions) if (typeof installerId !== "string" || !/^[UW][A-Z0-9]+$/.test(installerId) || installerId === result.bot_user_id) throw providerFailure("slack_install_account_missing"); if (row.managerGrantId) { const grant = await managerGrants.get(row.managerGrantId, row.companyId, actor); - if (installerId !== grant.slackUserId || teamId !== grant.workspaceId) throw providerFailure("slack_install_identity_mismatch"); + if (grant.status !== "active" || installerId !== grant.slackUserId || teamId !== grant.workspaceId) throw providerFailure("slack_install_identity_mismatch"); } await endpoint(row.endpointId, actor); // Reauthorization reuses the runtime signing secret after staging was cleaned. diff --git a/ui/storybook/fixtures/SlackSetupFixture.tsx b/ui/storybook/fixtures/SlackSetupFixture.tsx index 4e33331dbcb..38e95812608 100644 --- a/ui/storybook/fixtures/SlackSetupFixture.tsx +++ b/ui/storybook/fixtures/SlackSetupFixture.tsx @@ -1,4 +1,5 @@ import { useLayoutEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import { ChatEndpointSetup } from "@/pages/apps/chat/ChatEndpointSetup"; import { Button } from "@/components/ui/button"; import { ChatSetupSidebar } from "@/components/chat/ChatSetupNavigation"; @@ -7,10 +8,11 @@ import { type ChatEndpoint } from "@/api/chatEndpoints"; import { storybookAgents, storybookAuthSession } from "./paperclipData"; import { defaultSlackAppConfiguration } from "@paperclipai/shared"; -export type SlackSetupScenario = "choose" | "create" | "manual" | "install" | "declined" | "uncertain" | "manifest_pending" | "recovery" | "verify" | "avatar_failed" | "welcome_failed" | "success"; +export type SlackSetupScenario = "choose" | "create" | "manual" | "install" | "declined" | "approval_denied" | "uncertain" | "manifest_pending" | "recovery" | "verify" | "avatar_failed" | "welcome_failed" | "success"; /** Production wizard over a local provider fixture. No requests go to Slack. */ export function SlackSetupFixture({ scenario = "create", managed = false, managedAvailable = true, workspaceCount = 1 }: { scenario?: SlackSetupScenario; managed?: boolean; managedAvailable?: boolean; workspaceCount?: number }) { + const queryClient = useQueryClient(); const [ready, setReady] = useState(false); const [signalVerification, setSignalVerification] = useState<() => void>(() => () => {}); useLayoutEffect(() => { @@ -39,9 +41,10 @@ export function SlackSetupFixture({ scenario = "create", managed = false, manage endpoint.setup!.slackAccount = { externalUserId: "UPERSON", paperclipUserId: storybookAuthSession.user.id, status: "linked", welcomeStatus: scenario === "welcome_failed" ? "failed" : "sent", ...(scenario === "welcome_failed" ? {} : { dmChannelId: "DSTORY" }) }; endpoint.status = "verifying"; endpoint.providerAccountId = "TSTORY"; endpoint.botExternalId = "USTORY"; endpoint.botUsername = "maya"; }; - if (["install", "declined", "recovery", "manifest_pending"].includes(scenario)) created(); + if (["install", "declined", "approval_denied", "recovery", "manifest_pending"].includes(scenario)) created(); if (scenario === "manifest_pending") endpoint.setup!.slackRegistration!.errorCode = "slack_manifest_update_pending"; if (scenario === "declined") endpoint.setup!.slackRegistration!.errorCode = managed ? "slack_approval_pending" : "slack_install_declined"; + if (scenario === "approval_denied") endpoint.setup!.slackRegistration!.errorCode = "slack_approval_denied"; if (scenario === "recovery") endpoint.setup!.slackRegistration = { ...endpoint.setup!.slackRegistration!, status: "credentials_saved", errorCode: "slack_configuration_incomplete" }; if (scenario === "uncertain") endpoint.setup!.slackRegistration = { status: "uncertain", ...(managed ? { managerGrantId: grantId } : {}), errorCode: "slack_creation_uncertain", managementUrl: "https://api.slack.com/apps" }; if (["verify", "avatar_failed", "welcome_failed", "success"].includes(scenario)) installed(); @@ -57,6 +60,7 @@ export function SlackSetupFixture({ scenario = "create", managed = false, manage if (path === "/api/companies/company-storybook/chat-slack/setup-options") return Response.json({ managedAvailable: managed && managedAvailable, defaultMethod: managed && managedAvailable ? "managed" : "automatic", workspaces: managed && authorized ? Array.from({ length: Math.max(1, workspaceCount) }, (_, i) => ({ grantId: i ? "22222222-2222-4222-8222-222222222222" : grantId, workspaceId: `TSTORY${i}`, workspaceName: i ? "Research workspace" : "Paperclip", userId: "UPERSON" })) : [] }); if (path === "/api/chat-endpoints/slack-story/slack/managed/authorize") { authorized = true; + void queryClient.invalidateQueries({ queryKey: ["slack-managed-workspaces", endpoint.companyId] }); return Response.json({ authorizationUrl: `${window.location.href.split("#")[0]}#simulated-manager-consent`, expiresAt: new Date(Date.now() + 600_000).toISOString() }); } if (path === "/api/chat-endpoints/slack-story/slack/managed/provision") { installed(); return Response.json({}); } @@ -102,7 +106,7 @@ export function SlackSetupFixture({ scenario = "create", managed = false, manage }; setReady(true); return () => { window.fetch = original; }; - }, [scenario, managed, managedAvailable, workspaceCount]); + }, [scenario, managed, managedAvailable, workspaceCount, queryClient]); return