mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-03 17:54:05 +00:00
fix(ui): hide Registry when it is unavailable to the deployment (#12847)
This commit is contained in:
@@ -24,6 +24,7 @@ const REGISTRY_TASK_PATH_PREFIX = "/api/v1/tasks/";
|
||||
const REGISTRY_ERROR_CODE = {
|
||||
KEY_REJECTED: "registry_key_rejected",
|
||||
UNAVAILABLE: "registry_unavailable",
|
||||
PAGE_NOT_FOUND: "registry_page_not_found",
|
||||
} as const;
|
||||
// Opposite remedies, so a 409 is never read without its code.
|
||||
const REGISTRY_REMOVAL_CONFLICT_CODE = {
|
||||
@@ -216,18 +217,29 @@ export async function classifyRegistryFailure(
|
||||
return { status: REGISTRY_FAILURE.ERROR };
|
||||
}
|
||||
|
||||
// An enabled backend also answers 404 (missing page): tell them apart by code.
|
||||
export async function isRegistryDisabledResponse(response: Response) {
|
||||
if (response.status !== 404) return false;
|
||||
const codes = await getRegistryErrorCodes(response);
|
||||
return !codes.includes(REGISTRY_ERROR_CODE.PAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
function isRegistryDiscoveryEndpoint(endpoint: RegistryEndpoint) {
|
||||
return registryDiscoveryEndpoints.has(endpoint);
|
||||
}
|
||||
|
||||
async function getRegistryErrorCode(response: Response) {
|
||||
async function getRegistryErrorCodes(response: Response) {
|
||||
const parsed = errorDocumentSchema.safeParse(
|
||||
await response
|
||||
.clone()
|
||||
.json()
|
||||
.catch(() => undefined),
|
||||
);
|
||||
return parsed.success ? parsed.data.errors[0]?.code : undefined;
|
||||
return parsed.success ? parsed.data.errors.map(({ code }) => code) : [];
|
||||
}
|
||||
|
||||
async function getRegistryErrorCode(response: Response) {
|
||||
return (await getRegistryErrorCodes(response))[0];
|
||||
}
|
||||
|
||||
const REGISTRY_CATALOG_PAGE_SIZE = 100;
|
||||
|
||||
@@ -119,14 +119,17 @@ describe("installed Registry provider discovery", () => {
|
||||
emptyMetadata = false,
|
||||
failedEndpoint,
|
||||
failureStatus = 500,
|
||||
failureBody = {},
|
||||
}: {
|
||||
emptyMetadata?: boolean;
|
||||
failedEndpoint?: string;
|
||||
failureStatus?: number;
|
||||
failureBody?: unknown;
|
||||
} = {}) {
|
||||
fetchMock.mockImplementation((url: string) => {
|
||||
const endpoint = new URL(url).pathname.split("/").pop();
|
||||
if (endpoint === failedEndpoint) return jsonResponse({}, failureStatus);
|
||||
if (endpoint === failedEndpoint)
|
||||
return jsonResponse(failureBody, failureStatus);
|
||||
if (endpoint === "available-artifacts")
|
||||
return jsonResponse({
|
||||
data: [
|
||||
@@ -242,6 +245,45 @@ describe("installed Registry provider discovery", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["available-artifacts", "artifacts", "providers"])(
|
||||
"hides Registry when the backend has it disabled and %s answers 404",
|
||||
async (failedEndpoint) => {
|
||||
mockDiscovery({ failedEndpoint, failureStatus: 404 });
|
||||
expect(await getInstalledRegistryProviderOptions()).toEqual({
|
||||
status: "access_denied",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["available-artifacts", "providers"])(
|
||||
"keeps Registry visible when an enabled backend answers 404 for a missing %s page",
|
||||
async (failedEndpoint) => {
|
||||
mockDiscovery({
|
||||
failedEndpoint,
|
||||
failureStatus: 404,
|
||||
failureBody: {
|
||||
errors: [{ status: "404", code: "registry_page_not_found" }],
|
||||
},
|
||||
});
|
||||
expect(await getInstalledRegistryProviderOptions()).toEqual({
|
||||
status: "error",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps Registry visible when the missing-page code is not the first error", async () => {
|
||||
mockDiscovery({
|
||||
failedEndpoint: "available-artifacts",
|
||||
failureStatus: 404,
|
||||
failureBody: {
|
||||
errors: [{ code: "not_found" }, { code: "registry_page_not_found" }],
|
||||
},
|
||||
});
|
||||
expect(await getInstalledRegistryProviderOptions()).toEqual({
|
||||
status: "error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Registry guarded reads", () => {
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
classifyRegistryRemovalConflict,
|
||||
collectCompleteRegistryCatalog,
|
||||
isRegistryCollection,
|
||||
isRegistryDisabledResponse,
|
||||
parseRegistryArtifactSubmission,
|
||||
parseRegistryCredentialSubmission,
|
||||
RegistryCatalogPageError,
|
||||
@@ -86,6 +87,9 @@ async function readRegistryResponse(
|
||||
return { status: REGISTRY_FAILURE.ERROR };
|
||||
}
|
||||
if (response.ok) return response;
|
||||
// A backend with Registry disabled answers 404: hide it like a denial.
|
||||
if (await isRegistryDisabledResponse(response))
|
||||
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
||||
|
||||
return endpoint === REGISTRY_ENDPOINT.PROVIDERS ||
|
||||
endpoint === REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { Toaster } from "@/components/shadcn/toast/Toaster";
|
||||
import { resetToasts } from "@/components/shadcn/toast/use-toast";
|
||||
@@ -89,6 +89,8 @@ async function enterAccountDetails() {
|
||||
|
||||
describe("provider wizard account creation", () => {
|
||||
beforeEach(() => {
|
||||
// Registry discovery only runs in Cloud.
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
useProviderWizardStore.getState().reset();
|
||||
resetToasts();
|
||||
getInstalledRegistryProviderOptions.mockResolvedValue({
|
||||
@@ -97,6 +99,10 @@ describe("provider wizard account creation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("shows progress, blocks repeat clicks, and advances after creation", async () => {
|
||||
// Given
|
||||
let resolveCreation!: (value: typeof createdAccount) => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { addProvider, updateProvider, getInstalledRegistryProviderOptions } =
|
||||
vi.hoisted(() => ({
|
||||
@@ -86,6 +86,32 @@ describe("provider account aliases", () => {
|
||||
describe("Registry provider source tabs", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("never mentions Registry outside Cloud, even when discovery would fail", async () => {
|
||||
// Given
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
getInstalledRegistryProviderOptions.mockRejectedValue(new Error("network"));
|
||||
|
||||
// When
|
||||
render(<ConnectAccountForm onSuccess={vi.fn()} />);
|
||||
|
||||
// Then
|
||||
expect(
|
||||
await screen.findByRole("option", { name: /Amazon Web Services/ }),
|
||||
).toBeVisible();
|
||||
expect(getInstalledRegistryProviderOptions).not.toHaveBeenCalled();
|
||||
expect(
|
||||
screen.queryByText("Registry providers could not be loaded"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("tab", { name: "Registry" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the Registry tab when discovery denies access (Local or flag off)", async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
REGISTRY_PROVIDER_DISCOVERY,
|
||||
type RegistryProviderOption,
|
||||
} from "@/lib/registry/provider-options";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import {
|
||||
createAddProviderFormSchema,
|
||||
AddProviderFormValues,
|
||||
@@ -233,6 +234,8 @@ export const ConnectAccountForm = ({
|
||||
const createdAccount = useRef<ConnectAccountSuccessData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Registry is Cloud-only: elsewhere never ask, so a failure cannot surface it.
|
||||
if (!isCloud()) return;
|
||||
let active = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user