mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
588 lines
18 KiB
TypeScript
588 lines
18 KiB
TypeScript
"use server";
|
|
|
|
import { z } from "zod";
|
|
|
|
import { auth } from "@/auth.config";
|
|
import { apiBaseUrl } from "@/lib";
|
|
import { REGISTRY_ACCESS } from "@/lib/registry/access";
|
|
import {
|
|
evaluateRegistryAccess,
|
|
evaluateRegistryProviderAccess,
|
|
} from "@/lib/registry/access.server";
|
|
import { isActiveRegistryCredential } from "@/lib/registry/credential-task";
|
|
import { getRegistryNotInstallableMessage } from "@/lib/registry/installability";
|
|
import {
|
|
buildRegistryProviderOptions,
|
|
REGISTRY_PROVIDER_DISCOVERY,
|
|
type RegistryProviderDiscoveryResult,
|
|
} from "@/lib/registry/provider-options";
|
|
import {
|
|
REGISTRY_ARTIFACT_ACTION,
|
|
REGISTRY_BOOTSTRAP_STATE,
|
|
REGISTRY_CATALOG,
|
|
REGISTRY_CREDENTIAL_ACTION,
|
|
REGISTRY_CREDENTIAL_READ,
|
|
REGISTRY_ENDPOINT,
|
|
REGISTRY_FAILURE,
|
|
REGISTRY_MUTATION,
|
|
REGISTRY_SUBMISSION,
|
|
type RegistryAddArtifactInput,
|
|
type RegistryArtifactRemovalResult,
|
|
type RegistryBootstrapResult,
|
|
type RegistryBootstrapState,
|
|
type RegistryCollectionsResult,
|
|
type RegistryCredentialActionResult,
|
|
type RegistryCredentialReadResult,
|
|
type RegistryCredentialStatus,
|
|
type RegistryCredentialSubmitResult,
|
|
type RegistryFailureResult,
|
|
type RegistryMutationResult,
|
|
} from "@/types/registry";
|
|
|
|
import {
|
|
adaptRegistryCredentialStatus,
|
|
adaptRegistryTenantArtifacts,
|
|
classifyRegistryFailure,
|
|
classifyRegistryMutationRefusal,
|
|
classifyRegistryRemovalConflict,
|
|
collectCompleteRegistryCatalog,
|
|
isRegistryCollection,
|
|
isRegistryDisabledResponse,
|
|
parseRegistryArtifactSubmission,
|
|
parseRegistryCredentialSubmission,
|
|
RegistryCatalogPageError,
|
|
} from "./registry.adapter";
|
|
|
|
const REGISTRY_REQUEST_TIMEOUT_MS = 15_000;
|
|
|
|
async function getRegistryAccess(): Promise<string | null> {
|
|
const accessToken = (await auth())?.accessToken;
|
|
const access = await evaluateRegistryAccess(accessToken);
|
|
return access.status === REGISTRY_ACCESS.ELIGIBLE && accessToken?.trim()
|
|
? accessToken
|
|
: null;
|
|
}
|
|
|
|
async function readRegistryResponse(
|
|
accessToken: string,
|
|
resource: string,
|
|
endpoint: (typeof REGISTRY_ENDPOINT)[keyof typeof REGISTRY_ENDPOINT],
|
|
credential: RegistryCredentialStatus | null = null,
|
|
searchParams?: URLSearchParams,
|
|
): Promise<Response | RegistryFailureResult> {
|
|
const url = new URL(`${apiBaseUrl}/registry/${resource}`);
|
|
if (searchParams) url.search = searchParams.toString();
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url.toString(), {
|
|
cache: "no-store",
|
|
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
headers: {
|
|
Accept: "application/vnd.api+json",
|
|
Authorization: `Bearer ${accessToken}`,
|
|
},
|
|
});
|
|
} catch {
|
|
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
|
|
? classifyDiscoveryFailure(response, endpoint, accessToken, credential)
|
|
: classifyRegistryFailure(response, endpoint, credential);
|
|
}
|
|
|
|
async function readRegistryCredential(accessToken: string) {
|
|
const result = await readRegistryResponse(
|
|
accessToken,
|
|
"credential",
|
|
REGISTRY_ENDPOINT.CREDENTIAL,
|
|
);
|
|
if (!(result instanceof Response)) return result;
|
|
|
|
const credential = adaptRegistryCredentialStatus(
|
|
await result.json().catch(() => undefined),
|
|
);
|
|
return credential
|
|
? { status: REGISTRY_CREDENTIAL_READ.STATUS, credential }
|
|
: { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
|
|
async function readRegistryTenantArtifacts(accessToken: string) {
|
|
const result = await readRegistryResponse(
|
|
accessToken,
|
|
"artifacts",
|
|
REGISTRY_ENDPOINT.MUTATION,
|
|
);
|
|
if (!(result instanceof Response)) return result;
|
|
|
|
const tenantArtifacts = adaptRegistryTenantArtifacts(
|
|
await result.json().catch(() => undefined),
|
|
);
|
|
return tenantArtifacts
|
|
? { status: "ready" as const, tenantArtifacts }
|
|
: { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
|
|
async function classifyDiscoveryFailure(
|
|
response: Response,
|
|
endpoint:
|
|
| typeof REGISTRY_ENDPOINT.PROVIDERS
|
|
| typeof REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS,
|
|
accessToken: string,
|
|
credential: RegistryCredentialStatus | null,
|
|
) {
|
|
if (response.status === 409 && credential === null) {
|
|
const currentCredential = await readRegistryCredential(accessToken);
|
|
if (currentCredential.status === REGISTRY_FAILURE.ACCESS_DENIED) {
|
|
return currentCredential;
|
|
}
|
|
credential =
|
|
currentCredential.status === REGISTRY_CREDENTIAL_READ.STATUS
|
|
? currentCredential.credential
|
|
: null;
|
|
}
|
|
return classifyRegistryFailure(response, endpoint, credential);
|
|
}
|
|
|
|
async function readRegistryProviders(
|
|
accessToken: string,
|
|
credential: RegistryCredentialStatus | null,
|
|
) {
|
|
const result = await readRegistryResponse(
|
|
accessToken,
|
|
"providers",
|
|
REGISTRY_ENDPOINT.PROVIDERS,
|
|
credential,
|
|
);
|
|
if (!(result instanceof Response)) return result;
|
|
const payload = await result.json().catch(() => undefined);
|
|
const metadata = z
|
|
.object({
|
|
data: z.array(
|
|
z.object({
|
|
id: z.string(),
|
|
attributes: z
|
|
.object({
|
|
name: z.string().optional(),
|
|
logo_url: z.string().nullable().optional(),
|
|
})
|
|
.optional(),
|
|
}),
|
|
),
|
|
})
|
|
.safeParse(payload);
|
|
return isRegistryCollection(payload)
|
|
? {
|
|
status: "ready" as const,
|
|
providers: metadata.success
|
|
? metadata.data.data.map((provider) => ({
|
|
type: provider.id,
|
|
label: provider.attributes?.name || provider.id,
|
|
...(provider.attributes?.logo_url
|
|
? { logoUrl: provider.attributes.logo_url }
|
|
: {}),
|
|
}))
|
|
: [],
|
|
}
|
|
: { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
|
|
export async function getInstalledRegistryProviderOptions(): Promise<RegistryProviderDiscoveryResult> {
|
|
const access = (await auth())?.accessToken;
|
|
const permission = await evaluateRegistryProviderAccess(access);
|
|
if (permission.status === REGISTRY_ACCESS.UNKNOWN)
|
|
return { status: REGISTRY_PROVIDER_DISCOVERY.UNKNOWN };
|
|
if (!access || permission.status !== REGISTRY_ACCESS.ELIGIBLE)
|
|
return { status: REGISTRY_PROVIDER_DISCOVERY.ACCESS_DENIED };
|
|
const [catalog, installed, providers] = await Promise.all([
|
|
readCompleteRegistryCatalog(access, null),
|
|
readRegistryTenantArtifacts(access),
|
|
readRegistryProviders(access, null),
|
|
]);
|
|
if (
|
|
[catalog.status, installed.status, providers.status].some(
|
|
(status) => status === REGISTRY_FAILURE.ACCESS_DENIED,
|
|
)
|
|
)
|
|
return { status: REGISTRY_PROVIDER_DISCOVERY.ACCESS_DENIED };
|
|
if (
|
|
catalog.status !== REGISTRY_CATALOG.COMPLETE ||
|
|
installed.status !== "ready" ||
|
|
providers.status !== "ready"
|
|
)
|
|
return { status: REGISTRY_PROVIDER_DISCOVERY.ERROR };
|
|
return {
|
|
status: REGISTRY_PROVIDER_DISCOVERY.READY,
|
|
options: buildRegistryProviderOptions(
|
|
catalog.artifacts,
|
|
installed.tenantArtifacts,
|
|
providers.providers,
|
|
),
|
|
};
|
|
}
|
|
|
|
async function readCompleteRegistryCatalog(
|
|
accessToken: string,
|
|
credential: RegistryCredentialStatus | null,
|
|
) {
|
|
try {
|
|
return await collectCompleteRegistryCatalog(async (_page, searchParams) => {
|
|
const result = await readRegistryResponse(
|
|
accessToken,
|
|
"available-artifacts",
|
|
REGISTRY_ENDPOINT.AVAILABLE_ARTIFACTS,
|
|
credential,
|
|
searchParams,
|
|
);
|
|
if (!(result instanceof Response))
|
|
throw new RegistryCatalogPageError(result);
|
|
return result.json();
|
|
});
|
|
} catch (error) {
|
|
return error instanceof RegistryCatalogPageError
|
|
? error.failure
|
|
: { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
}
|
|
|
|
async function confirmRegistryMutation(
|
|
accessToken: string,
|
|
normalizedName: string,
|
|
shouldBePresent: boolean,
|
|
expectedVersion?: string,
|
|
): Promise<RegistryMutationResult> {
|
|
const tenantArtifacts = await readRegistryTenantArtifacts(accessToken);
|
|
if (tenantArtifacts.status === REGISTRY_FAILURE.ACCESS_DENIED)
|
|
return tenantArtifacts;
|
|
if (
|
|
tenantArtifacts.status !== "ready" ||
|
|
tenantArtifacts.tenantArtifacts.some(
|
|
(artifact) => artifact.normalizedName === normalizedName,
|
|
) !== shouldBePresent ||
|
|
(expectedVersion !== undefined &&
|
|
tenantArtifacts.tenantArtifacts.find(
|
|
(artifact) => artifact.normalizedName === normalizedName,
|
|
)?.resolvedVersion !== expectedVersion.trim())
|
|
) {
|
|
return { status: "refresh_failed" };
|
|
}
|
|
return {
|
|
status: "confirmed",
|
|
tenantArtifacts: tenantArtifacts.tenantArtifacts,
|
|
};
|
|
}
|
|
|
|
function bootstrapReady(
|
|
state: RegistryBootstrapState,
|
|
): RegistryBootstrapResult {
|
|
return { status: REGISTRY_BOOTSTRAP_STATE.READY, state };
|
|
}
|
|
|
|
function bootstrapFailure(
|
|
failure: RegistryFailureResult,
|
|
): RegistryBootstrapResult {
|
|
if (failure.status === REGISTRY_FAILURE.ACCESS_DENIED) {
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
}
|
|
return bootstrapReady({
|
|
status:
|
|
failure.status === REGISTRY_FAILURE.ONBOARDING
|
|
? REGISTRY_BOOTSTRAP_STATE.ERROR
|
|
: failure.status,
|
|
});
|
|
}
|
|
|
|
export async function getRegistryBootstrap(): Promise<RegistryBootstrapResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
|
|
const credentialRead = await readRegistryCredential(access);
|
|
if (credentialRead.status !== REGISTRY_CREDENTIAL_READ.STATUS) {
|
|
return bootstrapFailure(credentialRead);
|
|
}
|
|
const tenantArtifactsRead = await readRegistryTenantArtifacts(access);
|
|
if (tenantArtifactsRead.status !== "ready") {
|
|
return bootstrapFailure(tenantArtifactsRead);
|
|
}
|
|
|
|
const { credential } = credentialRead;
|
|
const { tenantArtifacts } = tenantArtifactsRead;
|
|
if (!isActiveRegistryCredential(credential)) {
|
|
return bootstrapReady({
|
|
status: credential.validationPending
|
|
? REGISTRY_BOOTSTRAP_STATE.VALIDATION_PENDING
|
|
: REGISTRY_BOOTSTRAP_STATE.ONBOARDING,
|
|
credential,
|
|
tenantArtifacts,
|
|
});
|
|
}
|
|
|
|
const catalog = await readCompleteRegistryCatalog(access, credential);
|
|
if (catalog.status === REGISTRY_FAILURE.ACCESS_DENIED) {
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
}
|
|
if (catalog.status === REGISTRY_CATALOG.INCOMPLETE) {
|
|
return bootstrapReady({
|
|
status: REGISTRY_BOOTSTRAP_STATE.INCOMPLETE,
|
|
catalog,
|
|
});
|
|
}
|
|
if (catalog.status !== REGISTRY_CATALOG.COMPLETE) {
|
|
return bootstrapFailure(catalog);
|
|
}
|
|
|
|
return bootstrapReady({
|
|
status: REGISTRY_BOOTSTRAP_STATE.READY,
|
|
credential,
|
|
catalog,
|
|
tenantArtifacts,
|
|
});
|
|
}
|
|
|
|
export async function refreshRegistryCredential(): Promise<RegistryCredentialReadResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
return readRegistryCredential(access);
|
|
}
|
|
|
|
export async function refreshRegistryCollections(): Promise<RegistryCollectionsResult> {
|
|
const access = (await auth())?.accessToken;
|
|
const permission = await evaluateRegistryAccess(access);
|
|
if (permission.status === REGISTRY_ACCESS.UNKNOWN)
|
|
return { status: REGISTRY_FAILURE.ERROR };
|
|
if (permission.status !== REGISTRY_ACCESS.ELIGIBLE || !access?.trim())
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
|
|
const catalog = await readCompleteRegistryCatalog(access, null);
|
|
if (catalog.status !== REGISTRY_CATALOG.COMPLETE) return catalog;
|
|
const tenantArtifactsRead = await readRegistryTenantArtifacts(access);
|
|
return tenantArtifactsRead.status === "ready"
|
|
? {
|
|
status: REGISTRY_CATALOG.COMPLETE,
|
|
catalog,
|
|
tenantArtifacts: tenantArtifactsRead.tenantArtifacts,
|
|
}
|
|
: tenantArtifactsRead;
|
|
}
|
|
|
|
export async function addRegistryArtifact({
|
|
normalizedName,
|
|
versionSpec,
|
|
}: RegistryAddArtifactInput): Promise<RegistryMutationResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED } as const;
|
|
if (
|
|
typeof normalizedName !== "string" ||
|
|
!normalizedName.trim() ||
|
|
(versionSpec !== undefined && typeof versionSpec !== "string")
|
|
)
|
|
return { status: REGISTRY_FAILURE.ERROR };
|
|
const catalog = await readCompleteRegistryCatalog(access, null);
|
|
if (catalog.status !== REGISTRY_CATALOG.COMPLETE) {
|
|
return catalog.status === REGISTRY_CATALOG.INCOMPLETE
|
|
? { status: REGISTRY_FAILURE.ERROR }
|
|
: catalog;
|
|
}
|
|
const artifact = catalog.artifacts.find(
|
|
(entry) => entry.normalizedName === normalizedName,
|
|
);
|
|
if (!artifact?.isInstallable)
|
|
return {
|
|
status: REGISTRY_MUTATION.REFUSED,
|
|
message: getRegistryNotInstallableMessage(artifact?.notInstallableReason),
|
|
};
|
|
const selectedVersion = versionSpec?.trim() || "latest";
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`${apiBaseUrl}/registry/artifacts`, {
|
|
method: "POST",
|
|
cache: "no-store",
|
|
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
headers: {
|
|
Accept: "application/vnd.api+json",
|
|
"Content-Type": "application/vnd.api+json",
|
|
Authorization: `Bearer ${access}`,
|
|
},
|
|
body: JSON.stringify({
|
|
data: {
|
|
type: "registry-artifacts",
|
|
attributes: {
|
|
normalized_name: normalizedName,
|
|
version_spec: selectedVersion,
|
|
},
|
|
},
|
|
}),
|
|
});
|
|
} catch {
|
|
return { status: REGISTRY_FAILURE.ERROR } as const;
|
|
}
|
|
if (response.status === 401 || response.status === 403) {
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED } as const;
|
|
}
|
|
if (response.status === 409) {
|
|
return { status: REGISTRY_FAILURE.ONBOARDING };
|
|
}
|
|
if (!response.ok) {
|
|
return (
|
|
(await classifyRegistryMutationRefusal(response)) ?? {
|
|
status: REGISTRY_FAILURE.ERROR,
|
|
}
|
|
);
|
|
}
|
|
|
|
const submission = await parseRegistryArtifactSubmission(response);
|
|
return submission.status === REGISTRY_SUBMISSION.PENDING
|
|
? { status: REGISTRY_ARTIFACT_ACTION.SUBMITTED, taskId: submission.taskId }
|
|
: { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
|
|
export async function confirmRegistryArtifactAddition(
|
|
normalizedName: string,
|
|
expectedVersion?: string,
|
|
): Promise<RegistryMutationResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
if (
|
|
expectedVersion !== undefined &&
|
|
(typeof expectedVersion !== "string" || !expectedVersion.trim())
|
|
) {
|
|
return { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
return confirmRegistryMutation(access, normalizedName, true, expectedVersion);
|
|
}
|
|
|
|
export async function removeRegistryArtifact(
|
|
normalizedName: string,
|
|
): Promise<RegistryArtifactRemovalResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(
|
|
`${apiBaseUrl}/registry/artifacts/${encodeURIComponent(normalizedName)}`,
|
|
{
|
|
method: "DELETE",
|
|
cache: "no-store",
|
|
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
headers: {
|
|
Accept: "application/vnd.api+json",
|
|
Authorization: `Bearer ${access}`,
|
|
},
|
|
},
|
|
);
|
|
} catch {
|
|
return { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
if (response.status === 401 || response.status === 403) {
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
}
|
|
if (response.status === 409) {
|
|
return (
|
|
(await classifyRegistryRemovalConflict(response)) ?? {
|
|
status: REGISTRY_FAILURE.ERROR,
|
|
}
|
|
);
|
|
}
|
|
if (!response.ok) return { status: REGISTRY_FAILURE.ERROR };
|
|
|
|
return confirmRegistryMutation(access, normalizedName, false);
|
|
}
|
|
|
|
export async function submitRegistryCredential(
|
|
key: string,
|
|
): Promise<RegistryCredentialSubmitResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
|
|
const priorCredential = await readRegistryCredential(access);
|
|
if (priorCredential.status !== REGISTRY_CREDENTIAL_READ.STATUS) {
|
|
return priorCredential;
|
|
}
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`${apiBaseUrl}/registry/credential`, {
|
|
method: "POST",
|
|
cache: "no-store",
|
|
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
headers: {
|
|
Accept: "application/vnd.api+json",
|
|
"Content-Type": "application/vnd.api+json",
|
|
Authorization: `Bearer ${access}`,
|
|
},
|
|
body: JSON.stringify({
|
|
data: {
|
|
type: "registry-credentials",
|
|
attributes: { api_key: key.trim() },
|
|
},
|
|
}),
|
|
});
|
|
} catch {
|
|
return { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
if (response.status === 401 || response.status === 403) {
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
}
|
|
|
|
// The task settles client-side through the task watcher; this action only
|
|
// hands back the verified task identity so the caller can watch it.
|
|
const submission = await parseRegistryCredentialSubmission(response);
|
|
if (submission.status !== REGISTRY_SUBMISSION.PENDING) {
|
|
return priorCredential.credential.configured
|
|
? {
|
|
status: REGISTRY_CREDENTIAL_ACTION.REPLACEMENT_FAILED,
|
|
credential: priorCredential.credential,
|
|
}
|
|
: { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
|
|
return {
|
|
status: REGISTRY_CREDENTIAL_ACTION.SUBMITTED,
|
|
taskId: submission.taskId,
|
|
priorConfigured: priorCredential.credential.configured,
|
|
};
|
|
}
|
|
|
|
export async function disconnectRegistryCredential(): Promise<RegistryCredentialActionResult> {
|
|
const access = await getRegistryAccess();
|
|
if (!access) return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(`${apiBaseUrl}/registry/credential`, {
|
|
method: "DELETE",
|
|
cache: "no-store",
|
|
signal: AbortSignal.timeout(REGISTRY_REQUEST_TIMEOUT_MS),
|
|
headers: {
|
|
Accept: "application/vnd.api+json",
|
|
Authorization: `Bearer ${access}`,
|
|
},
|
|
});
|
|
} catch {
|
|
return { status: REGISTRY_FAILURE.ERROR };
|
|
}
|
|
if (response.status === 401 || response.status === 403) {
|
|
return { status: REGISTRY_FAILURE.ACCESS_DENIED };
|
|
}
|
|
|
|
const credential = await readRegistryCredential(access);
|
|
const tenantArtifacts = await readRegistryTenantArtifacts(access);
|
|
if (credential.status !== REGISTRY_CREDENTIAL_READ.STATUS) return credential;
|
|
if (tenantArtifacts.status !== "ready") return tenantArtifacts;
|
|
if (!response.ok) return { status: REGISTRY_FAILURE.ERROR };
|
|
|
|
return {
|
|
status: REGISTRY_CREDENTIAL_ACTION.DISCONNECTED,
|
|
credential: credential.credential,
|
|
tenantArtifacts: tenantArtifacts.tenantArtifacts,
|
|
};
|
|
}
|