Compare commits

...
4 Commits
Author SHA1 Message Date
Pablo F.G c3420151b5 docs(ui): trim the Slack install-guard comment 2026-08-18 17:43:39 +02:00
Pablo F.G 6adbb75ca9 test(ui): pin the exact OAuth scopes Prowler asks Slack for 2026-08-18 17:40:26 +02:00
Pablo F.G 23d6c77a78 fix(ui): hide the decorative Slack icon from assistive technology 2026-08-18 17:39:03 +02:00
Pablo F.G 9d43aa0c33 fix(ui): confirm a Slack install only from a Slack-typed resource
- Require a non-empty id, the integrations resource type and the slack
  integration kind before reporting the workspace as installed
2026-08-18 17:38:40 +02:00
4 changed files with 32 additions and 11 deletions
+9
View File
@@ -248,10 +248,19 @@ describe("exchangeSlackOAuthCode result shape", () => {
["an array", []],
["a bare string", "invalid"],
["a resource with no id", { type: "integrations", attributes: {} }],
["a resource with an empty id", { ...INTEGRATION, id: "" }],
["a resource of another type", { ...INTEGRATION, type: "tasks" }],
[
"a resource with no attributes",
{ id: INTEGRATION.id, type: "integrations" },
],
[
"another kind of integration",
{
...INTEGRATION,
attributes: { ...INTEGRATION.attributes, integration_type: "jira" },
},
],
])("cannot confirm the install from %s", async (_label, data) => {
// Given — a 2xx whose `data` is truthy but is not an integration resource.
fetchMock.mockResolvedValue(exchangeResponse(data));
+20 -7
View File
@@ -12,7 +12,7 @@ import {
slackRateLimitMessage,
} from "@/lib/integrations/slack-errors";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
import type { IntegrationProps } from "@/types/integrations";
import { INTEGRATION_TYPE, type IntegrationProps } from "@/types/integrations";
interface SlackUnavailable {
unavailable: true;
@@ -98,23 +98,36 @@ const isSlackAuthorizeUrl = (value: string): boolean => {
}
};
const INTEGRATIONS_RESOURCE_TYPE = "integrations";
/**
* The callback names the workspace and redirects on this value alone, so a `2xx`
* payload that is not a JSON:API resource (`{}`, `[]`, `"invalid"`) must read as
* unreadable rather than as a connected workspace.
* unreadable rather than as a connected workspace. Identity too: a resource
* that is not a linkable Slack integration would be shown as the workspace
* just installed.
*/
const isIntegrationResource = (value: unknown): boolean => {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const { id, attributes } = value as Record<string, unknown>;
const { id, type, attributes } = value as Record<string, unknown>;
if (
typeof id !== "string" ||
id === "" ||
type !== INTEGRATIONS_RESOURCE_TYPE ||
typeof attributes !== "object" ||
attributes === null ||
Array.isArray(attributes)
) {
return false;
}
return (
typeof id === "string" &&
typeof attributes === "object" &&
attributes !== null &&
!Array.isArray(attributes)
(attributes as Record<string, unknown>).integration_type ===
INTEGRATION_TYPE.SLACK
);
};
@@ -43,9 +43,9 @@ describe("starting the install", () => {
expect(`${consentScreen.origin}${consentScreen.pathname}`).toBe(
"https://slack.com/oauth/v2/authorize",
);
expect((consentScreen.searchParams.get("scope") ?? "").split(",")).toEqual(
expect.arrayContaining(REQUIRED_SCOPES),
);
const scopes = (consentScreen.searchParams.get("scope") ?? "").split(",");
expect(scopes).toHaveLength(REQUIRED_SCOPES.length);
expect(scopes).toEqual(expect.arrayContaining(REQUIRED_SCOPES));
// The state is server-minted, binding this install to the session
// (design D5).
expect(consentScreen.searchParams.get("state")).toBeTruthy();
@@ -764,7 +764,6 @@ export const SlackIcon: React.FC<IconSvgProps> = ({
fill="none"
focusable="false"
height={height ?? size}
role="presentation"
viewBox="0 0 48 48"
width={width ?? size}
className={className}