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
This commit is contained in:
Pablo F.G
2026-07-14 09:02:21 +02:00
parent 35b3ff2c8e
commit b8e800ff8b
5 changed files with 152 additions and 8 deletions
@@ -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
@@ -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(
<DataTableRowActions
row={createRow(true)}
hasSelection={false}
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
onOpenProviderWizard={vi.fn()}
onOpenOrganizationWizard={vi.fn()}
capability={SCAN_SCHEDULE_CAPABILITY.MANUAL_ONLY}
/>,
);
// 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(
<DataTableRowActions
row={createRow(true)}
hasSelection={false}
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
onOpenProviderWizard={vi.fn()}
onOpenOrganizationWizard={vi.fn()}
capability={SCAN_SCHEDULE_CAPABILITY.BLOCKED}
/>,
);
// 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(
<DataTableRowActions
row={createRow(true)}
hasSelection={false}
isRowSelected={false}
testableProviderIds={[]}
onClearSelection={vi.fn()}
onOpenProviderWizard={vi.fn()}
onOpenOrganizationWizard={vi.fn()}
capability={SCAN_SCHEDULE_CAPABILITY.ADVANCED}
/>,
);
// 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");
@@ -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<EditScanScheduleState>({
@@ -590,11 +596,26 @@ export function DataTableRowActions({
)}
<div className="relative flex items-center justify-end gap-2">
<ActionDropdown>
<ActionDropdownItem
icon={<Pencil />}
label="Edit Provider Alias"
onSelect={() => setIsEditOpen(true)}
/>
{canEditAlias ? (
<ActionDropdownItem
icon={<Pencil />}
label="Edit Provider Alias"
onSelect={() => setIsEditOpen(true)}
/>
) : (
<ActionDropdownItem
icon={<Pencil />}
label={
<span className="flex flex-col items-start gap-1">
Edit Provider Alias
<Badge variant="warning" size="sm">
Requires subscription
</Badge>
</span>
}
disabled
/>
)}
<ActionDropdownItem
icon={<Timer />}
label="View Scan Jobs"
+23
View File
@@ -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,
+13
View File
@@ -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";