From 78a5fa139fffdb3df5ab41c39963a5fce6675d6c Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:20:03 +0100 Subject: [PATCH] feat(ui): gate API key creation behind Prowler Cloud (#13187) --- .../api-key-modals.integration.test.tsx | 63 ++++++++++++++++++ .../profile/api-keys-card-client.test.tsx | 64 +++++++++++++++++++ .../users/profile/api-keys-card-client.tsx | 33 ++++++++-- .../users/profile/create-api-key-modal.tsx | 2 +- ui/lib/cloud-upgrade.test.ts | 2 + ui/lib/cloud-upgrade.ts | 12 ++++ ui/types/cloud-upgrade.ts | 1 + 7 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 ui/components/users/profile/api-key-modals.integration.test.tsx create mode 100644 ui/components/users/profile/api-keys-card-client.test.tsx diff --git a/ui/components/users/profile/api-key-modals.integration.test.tsx b/ui/components/users/profile/api-key-modals.integration.test.tsx new file mode 100644 index 0000000000..bf89021934 --- /dev/null +++ b/ui/components/users/profile/api-key-modals.integration.test.tsx @@ -0,0 +1,63 @@ +import { describe, expect, vi } from "vitest"; + +import { it } from "@/__tests__/fixtures"; +import { render } from "@/__tests__/render-browser"; + +import type { EnrichedApiKey } from "./api-keys/types"; +import { CreateApiKeyModal } from "./create-api-key-modal"; +import { EditApiKeyNameModal } from "./edit-api-key-name-modal"; + +const apiKey: EnrichedApiKey = { + type: "api-keys", + id: "api-key-1", + attributes: { + name: "CI pipeline", + prefix: "pk_12345678", + expires_at: "2099-01-01T00:00:00Z", + revoked: false, + inserted_at: "2026-01-01T00:00:00Z", + last_used_at: null, + }, + relationships: { + entity: { data: { type: "users", id: "user-1" } }, + }, + userEmail: "user@example.com", +}; + +// The app runs under React Compiler; only this suite compiles like it does. +describe("API key modals under React Compiler", () => { + it("enables Create API Key once a name is typed", async () => { + // Given + const screen = await render( + , + ); + const submit = screen.getByRole("button", { name: "Create API Key" }); + await expect.element(submit).toBeDisabled(); + + // When + await screen.getByPlaceholder("My API Key").fill("CI pipeline"); + + // Then + await expect.element(submit).toBeEnabled(); + }); + + it("enables Save Changes once the name changes", async () => { + // Given + const screen = await render( + , + ); + const submit = screen.getByRole("button", { name: "Save Changes" }); + + // When + await screen.getByPlaceholder("My API Key").fill("Nightly export"); + + // Then + await expect.element(submit).toBeEnabled(); + }); +}); diff --git a/ui/components/users/profile/api-keys-card-client.test.tsx b/ui/components/users/profile/api-keys-card-client.test.tsx new file mode 100644 index 0000000000..118c39adea --- /dev/null +++ b/ui/components/users/profile/api-keys-card-client.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { useCloudUpgradeStore } from "@/store"; +import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade"; + +import { ApiKeysCardClient } from "./api-keys-card-client"; + +const { isCloudMock } = vi.hoisted(() => ({ + isCloudMock: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: vi.fn() }), +})); + +vi.mock("@/actions/api-keys/api-keys", () => ({ + createApiKey: vi.fn(), + updateApiKey: vi.fn(), + revokeApiKey: vi.fn(), +})); + +vi.mock("@/auth.config", () => ({ auth: vi.fn() })); + +vi.mock("@/lib/shared/env", () => ({ + isCloud: isCloudMock, +})); + +describe("ApiKeysCardClient", () => { + beforeEach(() => { + useCloudUpgradeStore.getState().closeCloudUpgrade(); + }); + + it("offers Prowler Cloud instead of creating keys on Local Server", async () => { + // Given + isCloudMock.mockReturnValue(false); + const user = userEvent.setup(); + render(); + + // When + await user.click(screen.getByRole("button", { name: /Create API Key/ })); + + // Then + expect(useCloudUpgradeStore.getState().activeFeature).toBe( + CLOUD_UPGRADE_FEATURE.API_KEYS, + ); + expect(screen.queryByLabelText(/Expires in/)).not.toBeInTheDocument(); + }); + + it("opens the create form in Prowler Cloud", async () => { + // Given + isCloudMock.mockReturnValue(true); + const user = userEvent.setup(); + render(); + + // When + await user.click(screen.getByRole("button", { name: /Create API Key/ })); + + // Then + expect(useCloudUpgradeStore.getState().activeFeature).toBeNull(); + expect(await screen.findByLabelText(/Expires in/)).toBeVisible(); + }); +}); diff --git a/ui/components/users/profile/api-keys-card-client.tsx b/ui/components/users/profile/api-keys-card-client.tsx index 0adc0c405a..a87c32a83f 100644 --- a/ui/components/users/profile/api-keys-card-client.tsx +++ b/ui/components/users/profile/api-keys-card-client.tsx @@ -11,9 +11,13 @@ import { CardHeader, CardTitle, } from "@/components/shadcn"; +import { Badge } from "@/components/shadcn/badge/badge"; import { CustomLink } from "@/components/shadcn/custom/custom-link"; import { DataTable } from "@/components/shadcn/table"; +import { isCloud } from "@/lib/shared/env"; +import { useCloudUpgradeStore } from "@/store"; import { MetaDataProps } from "@/types"; +import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade"; import { ApiKeySuccessModal } from "./api-key-success-modal"; import { createApiKeyColumns } from "./api-keys/column-api-keys"; @@ -32,6 +36,11 @@ export const ApiKeysCardClient = ({ metadata, }: ApiKeysCardClientProps) => { const router = useRouter(); + const openCloudUpgrade = useCloudUpgradeStore( + (state) => state.openCloudUpgrade, + ); + // API key creation is a Prowler Cloud feature; Local Server gets the upsell. + const canCreateApiKeys = isCloud(); const [selectedApiKey, setSelectedApiKey] = useState( null, ); @@ -48,6 +57,15 @@ export const ApiKeysCardClient = ({ router.refresh(); }; + const handleCreateClick = () => { + if (!canCreateApiKeys) { + openCloudUpgrade(CLOUD_UPGRADE_FEATURE.API_KEYS); + return; + } + + setIsCreateModalOpen(true); + }; + const handleRevokeSuccess = () => { router.refresh(); }; @@ -73,7 +91,14 @@ export const ApiKeysCardClient = ({
- API Keys +
+ API Keys + {!canCreateApiKeys && ( + + Cloud + + )} +

Manage API keys for programmatic access.{" "} @@ -82,11 +107,7 @@ export const ApiKeysCardClient = ({

- diff --git a/ui/components/users/profile/create-api-key-modal.tsx b/ui/components/users/profile/create-api-key-modal.tsx index e5dfa94fc3..cc569206f3 100644 --- a/ui/components/users/profile/create-api-key-modal.tsx +++ b/ui/components/users/profile/create-api-key-modal.tsx @@ -104,7 +104,7 @@ export const CreateApiKeyModal = ({

-
+
{ expect(titles).toEqual([ "Keep Every Provider Checked Automatically", "Turn Findings into Alerts", + "Automate Prowler with API Keys", "Add Your Entire AWS Organization", "Add Your Entire Azure Management Group", "Add Your Entire GCP Organization", @@ -74,6 +75,7 @@ describe("cloud upgrade URLs", () => { it.each([ [CLOUD_UPGRADE_FEATURE.ADVANCED_SCHEDULING, "advanced-scheduling"], [CLOUD_UPGRADE_FEATURE.ALERTS, "alerts"], + [CLOUD_UPGRADE_FEATURE.API_KEYS, "api-keys"], [CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS, "organization"], [CLOUD_UPGRADE_FEATURE.AZURE_ORGANIZATIONS, "azure-organization"], [CLOUD_UPGRADE_FEATURE.CLI_IMPORT, "cli-import"], diff --git a/ui/lib/cloud-upgrade.ts b/ui/lib/cloud-upgrade.ts index d6fd06eb39..9d63bd4c8d 100644 --- a/ui/lib/cloud-upgrade.ts +++ b/ui/lib/cloud-upgrade.ts @@ -27,6 +27,7 @@ const LOCAL_SERVER_UTM_SOURCE = "prowler-local-server"; const CLOUD_UPGRADE_UTM_CONTENT = { [CLOUD_UPGRADE_FEATURE.ADVANCED_SCHEDULING]: "advanced-scheduling", [CLOUD_UPGRADE_FEATURE.ALERTS]: "alerts", + [CLOUD_UPGRADE_FEATURE.API_KEYS]: "api-keys", [CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS]: "organization", [CLOUD_UPGRADE_FEATURE.AZURE_ORGANIZATIONS]: "azure-organization", [CLOUD_UPGRADE_FEATURE.CLI_IMPORT]: "cli-import", @@ -64,6 +65,17 @@ export const CLOUD_UPGRADE_CONTENT = { ], primaryCta: "Create Alerts in Prowler Cloud", }, + [CLOUD_UPGRADE_FEATURE.API_KEYS]: { + title: "Automate Prowler with API Keys", + description: + "Give scripts, pipelines, and integrations programmatic access to Prowler Cloud.", + benefits: [ + "Create named keys with their own expiration", + "Revoke a key without touching your user account", + "See when each key was last used", + ], + primaryCta: "Create API Keys in Prowler Cloud", + }, [CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS]: { title: "Add Your Entire AWS Organization", description: diff --git a/ui/types/cloud-upgrade.ts b/ui/types/cloud-upgrade.ts index e7b860a8d5..925922b564 100644 --- a/ui/types/cloud-upgrade.ts +++ b/ui/types/cloud-upgrade.ts @@ -1,6 +1,7 @@ export const CLOUD_UPGRADE_FEATURE = { ADVANCED_SCHEDULING: "advanced_scheduling", ALERTS: "alerts", + API_KEYS: "api_keys", AWS_ORGANIZATIONS: "aws_organizations", AZURE_ORGANIZATIONS: "azure_organizations", CLI_IMPORT: "cli_import",