mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +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 = {
|
const REGISTRY_ERROR_CODE = {
|
||||||
KEY_REJECTED: "registry_key_rejected",
|
KEY_REJECTED: "registry_key_rejected",
|
||||||
UNAVAILABLE: "registry_unavailable",
|
UNAVAILABLE: "registry_unavailable",
|
||||||
|
PAGE_NOT_FOUND: "registry_page_not_found",
|
||||||
} as const;
|
} as const;
|
||||||
// Opposite remedies, so a 409 is never read without its code.
|
// Opposite remedies, so a 409 is never read without its code.
|
||||||
const REGISTRY_REMOVAL_CONFLICT_CODE = {
|
const REGISTRY_REMOVAL_CONFLICT_CODE = {
|
||||||
@@ -216,18 +217,29 @@ export async function classifyRegistryFailure(
|
|||||||
return { status: REGISTRY_FAILURE.ERROR };
|
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) {
|
function isRegistryDiscoveryEndpoint(endpoint: RegistryEndpoint) {
|
||||||
return registryDiscoveryEndpoints.has(endpoint);
|
return registryDiscoveryEndpoints.has(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRegistryErrorCode(response: Response) {
|
async function getRegistryErrorCodes(response: Response) {
|
||||||
const parsed = errorDocumentSchema.safeParse(
|
const parsed = errorDocumentSchema.safeParse(
|
||||||
await response
|
await response
|
||||||
.clone()
|
.clone()
|
||||||
.json()
|
.json()
|
||||||
.catch(() => undefined),
|
.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;
|
const REGISTRY_CATALOG_PAGE_SIZE = 100;
|
||||||
|
|||||||
@@ -119,14 +119,17 @@ describe("installed Registry provider discovery", () => {
|
|||||||
emptyMetadata = false,
|
emptyMetadata = false,
|
||||||
failedEndpoint,
|
failedEndpoint,
|
||||||
failureStatus = 500,
|
failureStatus = 500,
|
||||||
|
failureBody = {},
|
||||||
}: {
|
}: {
|
||||||
emptyMetadata?: boolean;
|
emptyMetadata?: boolean;
|
||||||
failedEndpoint?: string;
|
failedEndpoint?: string;
|
||||||
failureStatus?: number;
|
failureStatus?: number;
|
||||||
|
failureBody?: unknown;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
fetchMock.mockImplementation((url: string) => {
|
fetchMock.mockImplementation((url: string) => {
|
||||||
const endpoint = new URL(url).pathname.split("/").pop();
|
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")
|
if (endpoint === "available-artifacts")
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
data: [
|
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", () => {
|
describe("Registry guarded reads", () => {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
classifyRegistryRemovalConflict,
|
classifyRegistryRemovalConflict,
|
||||||
collectCompleteRegistryCatalog,
|
collectCompleteRegistryCatalog,
|
||||||
isRegistryCollection,
|
isRegistryCollection,
|
||||||
|
isRegistryDisabledResponse,
|
||||||
parseRegistryArtifactSubmission,
|
parseRegistryArtifactSubmission,
|
||||||
parseRegistryCredentialSubmission,
|
parseRegistryCredentialSubmission,
|
||||||
RegistryCatalogPageError,
|
RegistryCatalogPageError,
|
||||||
@@ -86,6 +87,9 @@ async function readRegistryResponse(
|
|||||||
return { status: REGISTRY_FAILURE.ERROR };
|
return { status: REGISTRY_FAILURE.ERROR };
|
||||||
}
|
}
|
||||||
if (response.ok) return response;
|
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 ||
|
return endpoint === REGISTRY_ENDPOINT.PROVIDERS ||
|
||||||
endpoint === REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS
|
endpoint === REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
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 { Toaster } from "@/components/shadcn/toast/Toaster";
|
||||||
import { resetToasts } from "@/components/shadcn/toast/use-toast";
|
import { resetToasts } from "@/components/shadcn/toast/use-toast";
|
||||||
@@ -89,6 +89,8 @@ async function enterAccountDetails() {
|
|||||||
|
|
||||||
describe("provider wizard account creation", () => {
|
describe("provider wizard account creation", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
// Registry discovery only runs in Cloud.
|
||||||
|
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||||
useProviderWizardStore.getState().reset();
|
useProviderWizardStore.getState().reset();
|
||||||
resetToasts();
|
resetToasts();
|
||||||
getInstalledRegistryProviderOptions.mockResolvedValue({
|
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 () => {
|
it("shows progress, blocks repeat clicks, and advances after creation", async () => {
|
||||||
// Given
|
// Given
|
||||||
let resolveCreation!: (value: typeof createdAccount) => void;
|
let resolveCreation!: (value: typeof createdAccount) => void;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen, waitFor } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
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 } =
|
const { addProvider, updateProvider, getInstalledRegistryProviderOptions } =
|
||||||
vi.hoisted(() => ({
|
vi.hoisted(() => ({
|
||||||
@@ -86,6 +86,32 @@ describe("provider account aliases", () => {
|
|||||||
describe("Registry provider source tabs", () => {
|
describe("Registry provider source tabs", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
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 () => {
|
it("hides the Registry tab when discovery denies access (Local or flag off)", async () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
REGISTRY_PROVIDER_DISCOVERY,
|
REGISTRY_PROVIDER_DISCOVERY,
|
||||||
type RegistryProviderOption,
|
type RegistryProviderOption,
|
||||||
} from "@/lib/registry/provider-options";
|
} from "@/lib/registry/provider-options";
|
||||||
|
import { isCloud } from "@/lib/shared/env";
|
||||||
import {
|
import {
|
||||||
createAddProviderFormSchema,
|
createAddProviderFormSchema,
|
||||||
AddProviderFormValues,
|
AddProviderFormValues,
|
||||||
@@ -233,6 +234,8 @@ export const ConnectAccountForm = ({
|
|||||||
const createdAccount = useRef<ConnectAccountSuccessData | null>(null);
|
const createdAccount = useRef<ConnectAccountSuccessData | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Registry is Cloud-only: elsewhere never ask, so a failure cannot surface it.
|
||||||
|
if (!isCloud()) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user