From b8e800ff8b8f7e9cce2eafe024d17accf26d886f Mon Sep 17 00:00:00 2001 From: "Pablo F.G" Date: Tue, 14 Jul 2026 09:02:21 +0200 Subject: [PATCH] fix(ui): gate provider alias edit behind Cloud subscription - Disable "Edit Provider Alias" with a "Requires subscription" badge for Prowler Cloud accounts without an active subscription - Reuse the schedule capability as the billing-tier signal via a new canEditProviderAlias helper - Keep the action enabled in OSS/self-hosted and for subscribed accounts --- .../provider-alias-subscription-gate.fixed.md | 1 + .../table/data-table-row-actions.test.tsx | 86 +++++++++++++++++++ .../table/data-table-row-actions.tsx | 37 ++++++-- ui/lib/schedules.test.ts | 23 +++++ ui/lib/schedules.ts | 13 +++ 5 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 ui/changelog.d/provider-alias-subscription-gate.fixed.md diff --git a/ui/changelog.d/provider-alias-subscription-gate.fixed.md b/ui/changelog.d/provider-alias-subscription-gate.fixed.md new file mode 100644 index 0000000000..e88e5710b1 --- /dev/null +++ b/ui/changelog.d/provider-alias-subscription-gate.fixed.md @@ -0,0 +1 @@ +`Edit Provider Alias` is now disabled with a subscription badge for Prowler Cloud accounts without an active subscription, instead of failing on submit diff --git a/ui/components/providers/table/data-table-row-actions.test.tsx b/ui/components/providers/table/data-table-row-actions.test.tsx index 0a0a774b50..17a10fa8f4 100644 --- a/ui/components/providers/table/data-table-row-actions.test.tsx +++ b/ui/components/providers/table/data-table-row-actions.test.tsx @@ -468,6 +468,92 @@ describe("DataTableRowActions", () => { expect(screen.queryByText("Edit Scan Schedule")).not.toBeInTheDocument(); }); + it("disables Edit Provider Alias with a subscription badge for manual-only Cloud provider rows", async () => { + // Given a Prowler Cloud account without a subscription (trial/onboarding): + // editing the alias would be rejected by the API, so the UI must not offer it. + vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true"); + const user = userEvent.setup(); + + render( + , + ); + + // When + await user.click(screen.getByRole("button")); + + // Then the action is still discoverable but disabled with an upsell badge. + expect(screen.getByText("Requires subscription")).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /edit provider alias/i }), + ).toHaveAttribute("aria-disabled", "true"); + }); + + it("disables Edit Provider Alias for blocked Cloud provider rows", async () => { + // Given + vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true"); + const user = userEvent.setup(); + + render( + , + ); + + // When + await user.click(screen.getByRole("button")); + + // Then + expect(screen.getByText("Requires subscription")).toBeInTheDocument(); + expect( + screen.getByRole("menuitem", { name: /edit provider alias/i }), + ).toHaveAttribute("aria-disabled", "true"); + }); + + it("opens Edit Provider Alias for subscribed Cloud provider rows", async () => { + // Given a subscribed Cloud account (ADVANCED): the alias edit works. + vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true"); + const user = userEvent.setup(); + + render( + , + ); + + // When + await user.click(screen.getByRole("button")); + + // Then the item is enabled (no upsell badge) and opens the alias modal. + expect(screen.queryByText("Requires subscription")).not.toBeInTheDocument(); + await user.click(screen.getByText("Edit Provider Alias")); + expect( + screen.getByRole("dialog", { name: /edit provider alias/i }), + ).toBeInTheDocument(); + }); + it("opens scan config management with the precomputed current config id", async () => { // Given vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true"); diff --git a/ui/components/providers/table/data-table-row-actions.tsx b/ui/components/providers/table/data-table-row-actions.tsx index c43631f754..788012c950 100644 --- a/ui/components/providers/table/data-table-row-actions.tsx +++ b/ui/components/providers/table/data-table-row-actions.tsx @@ -27,6 +27,7 @@ import { type EditScanScheduleState, } from "@/components/scans/schedule/edit-scan-schedule-modal"; import { useToast } from "@/components/shadcn"; +import { Badge } from "@/components/shadcn/badge/badge"; import { ActionDropdown, ActionDropdownDangerZone, @@ -35,7 +36,10 @@ import { import { Modal } from "@/components/shadcn/modal"; import { runWithConcurrencyLimit } from "@/lib/concurrency"; import { testProviderConnection } from "@/lib/provider-helpers"; -import { getScanScheduleCapability } from "@/lib/schedules"; +import { + canEditProviderAlias, + getScanScheduleCapability, +} from "@/lib/schedules"; import { isCloud } from "@/lib/shared/env"; import { ORG_SETUP_PHASE, ORG_WIZARD_STEP } from "@/types/organizations"; import { PROVIDER_WIZARD_MODE } from "@/types/provider-wizard"; @@ -291,9 +295,11 @@ export function DataTableRowActions({ currentScanConfigId = null, capability, }: DataTableRowActionsProps) { + const resolvedCapability = capability ?? getScanScheduleCapability(isCloud()); const canEditSchedule = - (capability ?? getScanScheduleCapability(isCloud())) === - SCAN_SCHEDULE_CAPABILITY.ADVANCED; + resolvedCapability === SCAN_SCHEDULE_CAPABILITY.ADVANCED; + // Editing a provider alias requires a Prowler Cloud subscription + const canEditAlias = canEditProviderAlias(resolvedCapability); const [isEditOpen, setIsEditOpen] = useState(false); const [isScheduleOpen, setIsScheduleOpen] = useState(false); const [scheduleState, setScheduleState] = useState({ @@ -590,11 +596,26 @@ export function DataTableRowActions({ )}
- } - label="Edit Provider Alias" - onSelect={() => setIsEditOpen(true)} - /> + {canEditAlias ? ( + } + label="Edit Provider Alias" + onSelect={() => setIsEditOpen(true)} + /> + ) : ( + } + label={ + + Edit Provider Alias + + Requires subscription + + + } + disabled + /> + )} } label="View Scan Jobs" diff --git a/ui/lib/schedules.test.ts b/ui/lib/schedules.test.ts index 279a82881e..f65f0cca1e 100644 --- a/ui/lib/schedules.test.ts +++ b/ui/lib/schedules.test.ts @@ -5,6 +5,7 @@ import { buildScheduleAttributesFromProvider, buildSchedulesByProviderId, buildScheduleUpdatePayload, + canEditProviderAlias, formatDayOfMonth, formatScheduleHour, getBrowserTimezone, @@ -440,6 +441,28 @@ describe("scan schedule capability", () => { }); }); +describe("canEditProviderAlias", () => { + it("allows subscribed Cloud accounts (ADVANCED)", () => { + expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.ADVANCED)).toBe(true); + }); + + it("allows OSS / self-hosted (DAILY_LEGACY, no billing)", () => { + expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.DAILY_LEGACY)).toBe( + true, + ); + }); + + it("blocks Cloud trial/onboarding without a subscription (MANUAL_ONLY)", () => { + expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY)).toBe( + false, + ); + }); + + it("blocks over-limit Cloud accounts (BLOCKED)", () => { + expect(canEditProviderAlias(SCAN_SCHEDULE_CAPABILITY.BLOCKED)).toBe(false); + }); +}); + describe("buildSchedulesByProviderId", () => { const buildSchedule = ( id: string, diff --git a/ui/lib/schedules.ts b/ui/lib/schedules.ts index dd4cd0878d..edefb74c19 100644 --- a/ui/lib/schedules.ts +++ b/ui/lib/schedules.ts @@ -65,6 +65,19 @@ export function getScanScheduleCapability( : SCAN_SCHEDULE_CAPABILITY.DAILY_LEGACY; } +/** + * Whether the current account may edit a provider alias. Alias edits require a + * Prowler Cloud subscription + */ +export function canEditProviderAlias( + capability: ScanScheduleCapability, +): boolean { + return ( + capability === SCAN_SCHEDULE_CAPABILITY.ADVANCED || + capability === SCAN_SCHEDULE_CAPABILITY.DAILY_LEGACY + ); +} + export function formatScheduleHour(hour: number): string { const normalizedHour = ((hour % 24) + 24) % 24; const period = normalizedHour >= 12 ? "pm" : "am";