fix(ui): show alert permission errors (#11629)

This commit is contained in:
Alejandro Bailo
2026-06-17 15:44:52 +02:00
committed by GitHub
parent 73059ffc7e
commit 6546d51a6c
8 changed files with 204 additions and 12 deletions
+4
View File
@@ -4,6 +4,10 @@ All notable changes to the **Prowler UI** are documented in this file.
## [1.31.0] (Prowler UNRELEASED)
### 🚀 Added
- Controlled `402` and `403` Server Action error messages for alert seed and mutation flows [(#11629)](https://github.com/prowler-cloud/prowler/pull/11629)
### 🐞 Fixed
- Radio button no longer shifts vertically when selected [(#11608)](https://github.com/prowler-cloud/prowler/pull/11608)
@@ -14,6 +14,7 @@ import {
} from "@/app/(prowler)/alerts/_types";
import type { ProviderProps } from "@/types/providers";
import { ALERTS_PERMISSION_ERROR } from "../../_lib/alert-errors";
import { AlertFormModal } from "../alert-form-modal";
const recipientsActionMocks = vi.hoisted(() => ({
@@ -726,6 +727,28 @@ describe("AlertFormModal", () => {
).toHaveLength(1);
});
it("should show the manage alerts permission error when save seed is forbidden", async () => {
// Given
const user = userEvent.setup();
alertsActionMocks.seedAlertRule.mockResolvedValue({
error: "You do not have permission to perform this action.",
status: 403,
});
mockRecipientsList();
renderCreateModal({ editingAlert: createEditingAlert() });
// When
await user.click(screen.getByRole("button", { name: /^save$/i }));
// Then
expect(await screen.findByText(ALERTS_PERMISSION_ERROR)).toBeVisible();
expect(
screen.queryByText(
"Apply at least one alert-compatible Findings filter.",
),
).not.toBeInTheDocument();
});
it("should hydrate advanced edit mode filters and normalize them on save", async () => {
// Given
const user = userEvent.setup();
@@ -13,6 +13,7 @@ import type {
AlertFormValues,
} from "@/app/(prowler)/alerts/_types/alert-form";
import { ALERTS_PERMISSION_ERROR } from "../../_lib/alert-errors";
import { AlertsManager } from "../alerts-manager";
const actionMocks = vi.hoisted(() => ({
@@ -316,11 +317,7 @@ describe("AlertsManager", () => {
await user.click(screen.getByRole("button", { name: /submit alert/i }));
// Then
expect(
await screen.findByText(
"You don't have permission to manage alerts. Ask an administrator to update your role.",
),
).toBeVisible();
expect(await screen.findByText(ALERTS_PERMISSION_ERROR)).toBeVisible();
expect(toastMock).not.toHaveBeenCalled();
});
@@ -346,6 +343,32 @@ describe("AlertsManager", () => {
);
});
it("shows a manage alerts permission toast for disable 403 errors", async () => {
// Given
const user = userEvent.setup();
const alert = makeAlert(true);
actionMocks.disableAlert.mockResolvedValue({
error: "You do not have permission to perform this action.",
status: 403,
});
renderManager([alert]);
// When
await user.click(
screen.getByRole("button", { name: /actions for enabled alert/i }),
);
await user.click(screen.getByRole("menuitem", { name: /disable/i }));
// Then
await waitFor(() =>
expect(toastMock).toHaveBeenCalledWith({
variant: "destructive",
title: "Alert update failed",
description: ALERTS_PERMISSION_ERROR,
}),
);
});
it("shows a success toast after enabling an alert", async () => {
// Given
const user = userEvent.setup();
@@ -54,6 +54,7 @@ import {
getEmptyAlertFormDefaults,
getFindingsFiltersFromAlertCondition,
} from "../_lib/alert-adapter";
import { getAlertMutationError } from "../_lib/alert-errors";
import { alertFormSchema } from "../_lib/alert-form-schema";
import type {
AlertFormSubmitResult,
@@ -408,7 +409,7 @@ const AlertFormModalContent = ({
if (seedResult?.error) {
setPreview({
status: "error",
error: ALERT_SEED_ERROR,
error: getAlertMutationError(seedResult, ALERT_SEED_ERROR),
});
return;
}
@@ -452,7 +453,9 @@ const AlertFormModalContent = ({
: null;
if (seedResult?.error) {
setPreview(null);
setErrors({ root: ALERT_SEED_ERROR });
setErrors({
root: getAlertMutationError(seedResult, ALERT_SEED_ERROR),
});
return;
}
@@ -24,6 +24,7 @@ import type { ScanEntity } from "@/types";
import type { ProviderProps } from "@/types/providers";
import { toAlertPayload } from "../_lib/alert-adapter";
import { getAlertMutationError } from "../_lib/alert-errors";
import type {
AlertFormSubmitResult,
AlertFormValues,
@@ -49,8 +50,6 @@ interface AlertsManagerProps {
const ALERTS_FINDINGS_HREF =
"/findings?filter[muted]=false&filter[status__in]=FAIL";
const ALERTS_PERMISSION_ERROR =
"You don't have permission to manage alerts. Ask an administrator to update your role.";
export const AlertsManager = ({
alerts,
@@ -113,7 +112,7 @@ export const AlertsManager = ({
if (result?.error) {
return {
ok: false,
error: result.status === 403 ? ALERTS_PERMISSION_ERROR : result.error,
error: getAlertMutationError(result),
};
}
toast({
@@ -134,7 +133,7 @@ export const AlertsManager = ({
toast({
variant: "destructive",
title: "Alert update failed",
description: result.error,
description: getAlertMutationError(result),
});
return;
}
@@ -154,7 +153,7 @@ export const AlertsManager = ({
toast({
variant: "destructive",
title: "Alert delete failed",
description: result.error,
description: getAlertMutationError(result),
});
return;
}
@@ -0,0 +1,25 @@
import {
ACTION_ERROR_STATUS,
getActionErrorMessage,
} from "@/lib/action-errors";
export const ALERTS_PERMISSION_ERROR =
"You don't have permission to manage alerts. Ask an administrator to update your role.";
interface AlertActionErrorResult {
error: string;
status?: number;
}
export const getAlertMutationError = (
result: AlertActionErrorResult,
fallback = result.error,
): string =>
getActionErrorMessage(
{ ...result, error: fallback },
{
messages: {
[ACTION_ERROR_STATUS.FORBIDDEN]: ALERTS_PERMISSION_ERROR,
},
},
);
+72
View File
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import {
ACTION_ERROR_MESSAGES,
ACTION_ERROR_STATUS,
getActionErrorMessage,
} from "./action-errors";
describe("getActionErrorMessage", () => {
it("should use the default permission error for forbidden responses", () => {
// Given
const result = {
error: "You do not have permission to perform this action.",
status: ACTION_ERROR_STATUS.FORBIDDEN,
};
// When
const message = getActionErrorMessage(result);
// Then
expect(message).toBe(ACTION_ERROR_MESSAGES[ACTION_ERROR_STATUS.FORBIDDEN]);
});
it("should use the default subscription error for payment-required responses", () => {
// Given
const result = {
error: "Payment required.",
status: ACTION_ERROR_STATUS.PAYMENT_REQUIRED,
};
// When
const message = getActionErrorMessage(result);
// Then
expect(message).toBe(
ACTION_ERROR_MESSAGES[ACTION_ERROR_STATUS.PAYMENT_REQUIRED],
);
});
it("should use a feature override for handled statuses", () => {
// Given
const result = {
error: "You do not have permission to perform this action.",
status: ACTION_ERROR_STATUS.FORBIDDEN,
};
const override = "You don't have permission to manage alerts.";
// When
const message = getActionErrorMessage(result, {
messages: {
[ACTION_ERROR_STATUS.FORBIDDEN]: override,
},
});
// Then
expect(message).toBe(override);
});
it("should keep the API error for unhandled statuses", () => {
// Given
const result = {
error: "Apply at least one alert-compatible Findings filter.",
status: 400,
};
// When
const message = getActionErrorMessage(result);
// Then
expect(message).toBe(result.error);
});
});
+43
View File
@@ -0,0 +1,43 @@
export const ACTION_ERROR_STATUS = {
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
} as const;
export type ActionErrorStatus =
(typeof ACTION_ERROR_STATUS)[keyof typeof ACTION_ERROR_STATUS];
export const ACTION_ERROR_MESSAGES = {
[ACTION_ERROR_STATUS.PAYMENT_REQUIRED]:
"Your subscription doesn't allow this action. Upgrade your plan or contact an administrator.",
[ACTION_ERROR_STATUS.FORBIDDEN]:
"You don't have permission to perform this action. Ask an administrator to update your role.",
} as const satisfies Record<ActionErrorStatus, string>;
interface ActionErrorResult {
error?: string;
status?: number;
}
interface GetActionErrorMessageOptions {
messages?: Partial<Record<ActionErrorStatus, string>>;
fallback?: string;
}
const isActionErrorStatus = (
status: number | undefined,
): status is ActionErrorStatus =>
status === ACTION_ERROR_STATUS.PAYMENT_REQUIRED ||
status === ACTION_ERROR_STATUS.FORBIDDEN;
export const getActionErrorMessage = (
result: ActionErrorResult,
options: GetActionErrorMessageOptions = {},
): string => {
if (isActionErrorStatus(result.status)) {
return (
options.messages?.[result.status] ?? ACTION_ERROR_MESSAGES[result.status]
);
}
return result.error ?? options.fallback ?? "Oops! Something went wrong.";
};