mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
feat(ui): add okta provider support (#11213)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
1 parent
6eebfcfe77
commit
1f3f5c2e27
24 files changed
+341
-5
No files matched your search
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.28.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `okta` provider support with OAuth 2.0 private-key JWT credentials form (client ID + PEM private key) [(#11213)](https://github.com/prowler-cloud/prowler/pull/11213)
|
||||
|
||||
---
|
||||
|
||||
## [1.27.0] (Prowler v5.27.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -29,7 +29,7 @@ vi.mock("@/lib", () => ({
|
||||
wait: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/provider-credentials/build-crendentials", () => ({
|
||||
vi.mock("@/lib/provider-credentials/build-credentials", () => ({
|
||||
buildSecretConfig: vi.fn(() => ({
|
||||
secretType: "access-secret-key",
|
||||
secret: { key: "value" },
|
||||
|
||||
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders, getFormValue, wait } from "@/lib";
|
||||
import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials";
|
||||
import { buildSecretConfig } from "@/lib/provider-credentials/build-credentials";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { appendSanitizedProviderInFilters } from "@/lib/provider-filters";
|
||||
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("@/components/icons/providers-badge", () => ({
|
||||
OracleCloudProviderBadge: () => <span>Oracle Cloud</span>,
|
||||
AlibabaCloudProviderBadge: () => <span>Alibaba Cloud</span>,
|
||||
VercelProviderBadge: () => <span>Vercel</span>,
|
||||
OktaProviderBadge: () => <span>Okta</span>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/select/multiselect", () => ({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OktaProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
@@ -51,6 +52,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
|
||||
cloudflare: <CloudflareProviderBadge width={18} height={18} />,
|
||||
openstack: <OpenStackProviderBadge width={18} height={18} />,
|
||||
vercel: <VercelProviderBadge width={18} height={18} />,
|
||||
okta: <OktaProviderBadge width={18} height={18} />,
|
||||
};
|
||||
|
||||
/** Common props shared by both batch and instant modes. */
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("@/components/icons/providers-badge", () => ({
|
||||
CloudflareProviderBadge: () => <span>Cloudflare</span>,
|
||||
OpenStackProviderBadge: () => <span>OpenStack</span>,
|
||||
VercelProviderBadge: () => <span>Vercel</span>,
|
||||
OktaProviderBadge: () => <span>Okta</span>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/select/multiselect", () => ({
|
||||
|
||||
@@ -89,6 +89,11 @@ const VercelProviderBadge = lazy(() =>
|
||||
default: m.VercelProviderBadge,
|
||||
})),
|
||||
);
|
||||
const OktaProviderBadge = lazy(() =>
|
||||
import("@/components/icons/providers-badge").then((m) => ({
|
||||
default: m.OktaProviderBadge,
|
||||
})),
|
||||
);
|
||||
|
||||
type IconProps = { width: number; height: number };
|
||||
|
||||
@@ -160,6 +165,10 @@ const PROVIDER_DATA: Record<
|
||||
label: "Vercel",
|
||||
icon: VercelProviderBadge,
|
||||
},
|
||||
okta: {
|
||||
label: "Okta",
|
||||
icon: OktaProviderBadge,
|
||||
},
|
||||
};
|
||||
|
||||
/** Common props shared by both batch and instant modes. */
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OktaProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
@@ -175,6 +176,11 @@ const KNOWN_NODE_VISUALS = {
|
||||
description: "Vercel Account",
|
||||
Icon: VercelProviderBadge,
|
||||
},
|
||||
oktaaccount: {
|
||||
category: NODE_CATEGORY.ACCOUNT,
|
||||
description: "Okta Account",
|
||||
Icon: OktaProviderBadge,
|
||||
},
|
||||
s3bucket: {
|
||||
category: NODE_CATEGORY.STORAGE,
|
||||
description: "S3 Bucket",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OktaProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
@@ -34,6 +35,7 @@ export const PROVIDER_ICONS = {
|
||||
cloudflare: CloudflareProviderBadge,
|
||||
openstack: OpenStackProviderBadge,
|
||||
vercel: VercelProviderBadge,
|
||||
okta: OktaProviderBadge,
|
||||
} as const;
|
||||
|
||||
interface ProviderIconCellProps {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ImageProviderBadge } from "./image-provider-badge";
|
||||
import { KS8ProviderBadge } from "./ks8-provider-badge";
|
||||
import { M365ProviderBadge } from "./m365-provider-badge";
|
||||
import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge";
|
||||
import { OktaProviderBadge } from "./okta-provider-badge";
|
||||
import { OpenStackProviderBadge } from "./openstack-provider-badge";
|
||||
import { OracleCloudProviderBadge } from "./oraclecloud-provider-badge";
|
||||
import { VercelProviderBadge } from "./vercel-provider-badge";
|
||||
@@ -31,6 +32,7 @@ export {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OktaProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
@@ -53,4 +55,5 @@ export const PROVIDER_BADGE_BY_NAME: Record<string, FC<IconSvgProps>> = {
|
||||
Cloudflare: CloudflareProviderBadge,
|
||||
OpenStack: OpenStackProviderBadge,
|
||||
Vercel: VercelProviderBadge,
|
||||
Okta: OktaProviderBadge,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
const WHEEL_PATH =
|
||||
"M34.6,0.4l-1.3,16c-0.6-0.1-1.2-0.1-1.9-0.1c-0.8,0-1.6,0.1-2.3,0.2l-0.7-7.7c0-0.2,0.2-0.5,0.4-0.5h1.3l-0.6-7.8c0-0.2,0.2-0.5,0.4-0.5h4.3C34.5,0,34.7,0.2,34.6,0.4L34.6,0.4L34.6,0.4z M23.8,1.2c-0.1-0.2-0.3-0.4-0.5-0.3l-4,1.5C19,2.5,18.9,2.8,19,3l3.3,7.1l-1.2,0.5c-0.2,0.1-0.3,0.3-0.2,0.6l3.3,7c1.2-0.7,2.5-1.2,3.9-1.5L23.8,1.2L23.8,1.2z M14,5.7l9.3,13.1c-1.2,0.8-2.2,1.7-3.1,2.7L14.5,16c-0.2-0.2-0.2-0.5,0-0.6l1-0.8L10,9c-0.2-0.2-0.2-0.5,0-0.6l3.3-2.7C13.5,5.4,13.8,5.5,14,5.7L14,5.7z M6.2,13.2c-0.2-0.1-0.5-0.1-0.6,0.1l-2.1,3.7c-0.1,0.2,0,0.5,0.2,0.6l7.1,3.4l-0.7,1.1c-0.1,0.2,0,0.5,0.2,0.6l7.1,3.2c0.5-1.3,1.2-2.5,2-3.6L6.2,13.2z M0.9,23.3c0-0.2,0.3-0.4,0.5-0.3l15.5,4c-0.4,1.3-0.6,2.7-0.7,4.1l-7.8-0.6c-0.2,0-0.4-0.2-0.4-0.5l0.2-1.3L0.6,28c-0.2,0-0.4-0.2-0.4-0.5L0.9,23.3L0.9,23.3L0.9,23.3z M0.4,33.8C0.1,33.8,0,34,0,34.3l0.8,4.2c0,0.2,0.3,0.4,0.5,0.3l7.6-2l0.2,1.3c0,0.2,0.3,0.4,0.5,0.3l7.5-2.1c-0.4-1.3-0.7-2.7-0.8-4.1L0.4,33.8L0.4,33.8z M2.9,44.9c-0.1-0.2,0-0.5,0.2-0.6l14.5-6.9c0.5,1.3,1.3,2.5,2.2,3.6l-6.3,4.5c-0.2,0.1-0.5,0.1-0.6-0.1L12,44.3l-6.5,4.5c-0.2,0.1-0.5,0.1-0.6-0.1L2.9,44.9L2.9,44.9z M20.4,41.9L9.1,53.3c-0.2,0.2-0.2,0.5,0,0.6l3.3,2.7c0.2,0.2,0.5,0.1,0.6-0.1l4.6-6.4l1,0.9c0.2,0.2,0.5,0.1,0.6-0.1l4.4-6.4C22.4,43.8,21.3,42.9,20.4,41.9L20.4,41.9z M18.2,60.1c-0.2-0.1-0.3-0.3-0.2-0.6L24.6,45c1.2,0.6,2.6,1.1,3.9,1.4l-2,7.5c-0.1,0.2-0.3,0.4-0.5,0.3l-1.2-0.5l-2.1,7.6c-0.1,0.2-0.3,0.4-0.5,0.3L18.2,60.1L18.2,60.1L18.2,60.1z M29.6,46.6l-1.3,16c0,0.2,0.2,0.5,0.4,0.5H33c0.2,0,0.4-0.2,0.4-0.5l-0.6-7.8h1.3c0.2,0,0.4-0.2,0.4-0.5l-0.7-7.7c-0.8,0.1-1.5,0.2-2.3,0.2C30.9,46.7,30.2,46.7,29.6,46.6L29.6,46.6z M45.1,3.4c0.1-0.2,0-0.5-0.2-0.6l-4-1.5c-0.2-0.1-0.5,0.1-0.5,0.3l-2.1,7.6l-1.2-0.5c-0.2-0.1-0.5,0.1-0.5,0.3l-2,7.5c1.4,0.3,2.7,0.8,3.9,1.4L45.1,3.4L45.1,3.4z M53.9,9.7L42.6,21.1c-0.9-1-2-1.9-3.2-2.6l4.4-6.4c0.1-0.2,0.4-0.2,0.6-0.1l1,0.9l4.6-6.4c0.1-0.2,0.4-0.2,0.6-0.1l3.3,2.7C54,9.3,54,9.6,53.9,9.7L53.9,9.7z M59.9,18.7c0.2-0.1,0.3-0.4,0.2-0.6L58,14.4c-0.1-0.2-0.4-0.3-0.6-0.1l-6.5,4.5l-0.7-1.1c-0.1-0.2-0.4-0.3-0.6-0.1L43.3,22c0.9,1.1,1.6,2.3,2.2,3.6L59.9,18.7L59.9,18.7z M62.2,24.5l0.7,4.2c0,0.2-0.1,0.5-0.4,0.5l-15.9,1.5c-0.1-1.4-0.4-2.8-0.8-4.1l7.5-2.1c0.2-0.1,0.5,0.1,0.5,0.3l0.2,1.3l7.6-2C61.9,24.1,62.1,24.3,62.2,24.5L62.2,24.5L62.2,24.5z M61.5,40c0.2,0.1,0.5-0.1,0.5-0.3l0.7-4.2c0-0.2-0.1-0.5-0.4-0.5l-7.8-0.7l0.2-1.3c0-0.2-0.1-0.5-0.4-0.5l-7.8-0.6c0,1.4-0.3,2.8-0.7,4.1L61.5,40L61.5,40L61.5,40z M57.4,49.6c-0.1,0.2-0.4,0.3-0.6,0.1l-13.2-9.1c0.8-1.1,1.5-2.3,2-3.6l7.1,3.2c0.2,0.1,0.3,0.4,0.2,0.6L52.2,42l7.1,3.4c0.2,0.1,0.3,0.4,0.2,0.6L57.4,49.6C57.4,49.6,57.4,49.6,57.4,49.6z M39.7,44.2L49,57.3c0.1,0.2,0.4,0.2,0.6,0.1l3.3-2.7c0.2-0.2,0.2-0.4,0-0.6l-5.5-5.6l1-0.8c0.2-0.2,0.2-0.4,0-0.6l-5.5-5.5C42,42.6,40.9,43.5,39.7,44.2L39.7,44.2L39.7,44.2z M39.7,62c-0.2,0.1-0.5-0.1-0.5-0.3l-4.2-15.4c1.4-0.3,2.7-0.8,3.9-1.5l3.3,7c0.1,0.2,0,0.5-0.2,0.6l-1.2,0.5l3.3,7.1c0.1,0.2,0,0.5-0.2,0.6L39.7,62L39.7,62L39.7,62z";
|
||||
|
||||
export const OktaProviderBadge: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const w = size || width;
|
||||
const h = size || height;
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{ display: "inline-block", width: w, height: h }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const fill = resolvedTheme === "dark" ? "#FFFFFF" : "#191919";
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 63 63"
|
||||
width={w}
|
||||
height={h}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
<path fill={fill} d={WHEEL_PATH} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OktaProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
@@ -103,6 +104,11 @@ const PROVIDERS = [
|
||||
label: "Vercel",
|
||||
badge: VercelProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "okta",
|
||||
label: "Okta",
|
||||
badge: OktaProviderBadge,
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface RadioGroupProviderProps {
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
M365ClientSecretCredentials,
|
||||
MongoDBAtlasCredentials,
|
||||
OCICredentials,
|
||||
OktaCredentials,
|
||||
OpenStackCredentials,
|
||||
ProviderType,
|
||||
VercelCredentials,
|
||||
@@ -59,6 +60,7 @@ import { IacCredentialsForm } from "./via-credentials/iac-credentials-form";
|
||||
import { ImageCredentialsForm } from "./via-credentials/image-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { MongoDBAtlasCredentialsForm } from "./via-credentials/mongodbatlas-credentials-form";
|
||||
import { OktaCredentialsForm } from "./via-credentials/okta-credentials-form";
|
||||
import { OpenStackCredentialsForm } from "./via-credentials/openstack-credentials-form";
|
||||
import { OracleCloudCredentialsForm } from "./via-credentials/oraclecloud-credentials-form";
|
||||
import { VercelCredentialsForm } from "./via-credentials/vercel-credentials-form";
|
||||
@@ -279,6 +281,11 @@ export const BaseCredentialsForm = ({
|
||||
control={form.control as unknown as Control<VercelCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "okta" && (
|
||||
<OktaCredentialsForm
|
||||
control={form.control as unknown as Control<OktaCredentials>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideActions && (
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
|
||||
@@ -121,6 +121,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
|
||||
label: "Team ID",
|
||||
placeholder: "e.g. team_xxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
};
|
||||
case "okta":
|
||||
return {
|
||||
label: "Org Domain",
|
||||
placeholder: "e.g. your-org.okta.com",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: "Provider UID",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import {
|
||||
WizardInputField,
|
||||
WizardTextareaField,
|
||||
} from "@/components/providers/workflow/forms/fields";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { OktaCredentials } from "@/types";
|
||||
|
||||
export const OktaCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<OktaCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Connect via OAuth 2.0 Private Key JWT
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Provide the Client ID and PEM-encoded private key of an Okta API
|
||||
Services app whose matching public key (JWK) is registered on the
|
||||
service app.
|
||||
</div>
|
||||
</div>
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.OKTA_CLIENT_ID}
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="e.g. 0oa123456789abcdef"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<WizardTextareaField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.OKTA_PRIVATE_KEY}
|
||||
label="Private Key"
|
||||
labelPlacement="inside"
|
||||
placeholder="Paste your Okta app private key here"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
<div className="text-default-400 text-xs">
|
||||
The private key is sent over TLS and stored as a secret in the backend.
|
||||
You can rotate or revoke the public key from the Okta admin console at
|
||||
any time.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OktaProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
VercelProviderBadge,
|
||||
@@ -49,6 +50,8 @@ export const getProviderLogo = (provider: ProviderType) => {
|
||||
return <OpenStackProviderBadge width={35} height={35} />;
|
||||
case "vercel":
|
||||
return <VercelProviderBadge width={35} height={35} />;
|
||||
case "okta":
|
||||
return <OktaProviderBadge width={35} height={35} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -86,6 +89,8 @@ export const getProviderName = (provider: ProviderType): string => {
|
||||
return "OpenStack";
|
||||
case "vercel":
|
||||
return "Vercel";
|
||||
case "okta":
|
||||
return "Okta";
|
||||
default:
|
||||
return "Unknown Provider";
|
||||
}
|
||||
|
||||
@@ -248,6 +248,12 @@ export const useCredentialsForm = ({
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.VERCEL_API_TOKEN]: "",
|
||||
};
|
||||
case "okta":
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.OKTA_CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.OKTA_PRIVATE_KEY]: "",
|
||||
};
|
||||
default:
|
||||
return baseDefaults;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@ export const getProviderHelpText = (provider: string) => {
|
||||
text: "Need help connecting your Vercel team?",
|
||||
link: "https://goto.prowler.com/provider-vercel",
|
||||
};
|
||||
case "okta":
|
||||
return {
|
||||
text: "Need help connecting your Okta organization?",
|
||||
link: "https://goto.prowler.com/provider-okta",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: "How to setup a provider?",
|
||||
|
||||
+18
@@ -260,6 +260,20 @@ export const buildVercelSecret = (formData: FormData) => {
|
||||
return filterEmptyValues(secret);
|
||||
};
|
||||
|
||||
export const buildOktaSecret = (formData: FormData) => {
|
||||
const secret = {
|
||||
[ProviderCredentialFields.OKTA_CLIENT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.OKTA_CLIENT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.OKTA_PRIVATE_KEY]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.OKTA_PRIVATE_KEY,
|
||||
),
|
||||
};
|
||||
return filterEmptyValues(secret);
|
||||
};
|
||||
|
||||
export const buildOpenStackSecret = (formData: FormData) => {
|
||||
const secret = {
|
||||
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: getFormValue(
|
||||
@@ -513,6 +527,10 @@ export const buildSecretConfig = (
|
||||
secretType: "static",
|
||||
secret: buildVercelSecret(formData),
|
||||
}),
|
||||
okta: () => ({
|
||||
secretType: "static",
|
||||
secret: buildOktaSecret(formData),
|
||||
}),
|
||||
};
|
||||
|
||||
const builder = secretBuilders[providerType];
|
||||
@@ -91,6 +91,10 @@ export const ProviderCredentialFields = {
|
||||
|
||||
// Vercel fields
|
||||
VERCEL_API_TOKEN: "api_token",
|
||||
|
||||
// Okta fields
|
||||
OKTA_CLIENT_ID: "okta_client_id",
|
||||
OKTA_PRIVATE_KEY: "okta_private_key",
|
||||
} as const;
|
||||
|
||||
// Type for credential field values
|
||||
@@ -150,6 +154,8 @@ export const ErrorPointers = {
|
||||
"/data/attributes/secret/credentials_content",
|
||||
GOOGLEWORKSPACE_DELEGATED_USER: "/data/attributes/secret/delegated_user",
|
||||
VERCEL_API_TOKEN: "/data/attributes/secret/api_token",
|
||||
OKTA_CLIENT_ID: "/data/attributes/secret/okta_client_id",
|
||||
OKTA_PRIVATE_KEY: "/data/attributes/secret/okta_private_key",
|
||||
} as const;
|
||||
|
||||
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
|
||||
@@ -389,6 +389,12 @@ export type VercelCredentials = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type OktaCredentials = {
|
||||
[ProviderCredentialFields.OKTA_CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.OKTA_PRIVATE_KEY]: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type CredentialsFormSchema =
|
||||
| AWSCredentials
|
||||
| AWSCredentialsRole
|
||||
@@ -406,7 +412,8 @@ export type CredentialsFormSchema =
|
||||
| CloudflareCredentials
|
||||
| OpenStackCredentials
|
||||
| GoogleWorkspaceCredentials
|
||||
| VercelCredentials;
|
||||
| VercelCredentials
|
||||
| OktaCredentials;
|
||||
|
||||
export interface SearchParamsProps {
|
||||
[key: string]: string | string[] | undefined;
|
||||
|
||||
@@ -2,7 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
|
||||
import { addCredentialsRoleFormSchema } from "./formSchemas";
|
||||
import {
|
||||
addCredentialsFormSchema,
|
||||
addCredentialsRoleFormSchema,
|
||||
addProviderFormSchema,
|
||||
} from "./formSchemas";
|
||||
|
||||
const BASE_AWS_ROLE_VALUES = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: "provider-123",
|
||||
@@ -46,3 +50,103 @@ describe("addCredentialsRoleFormSchema", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addProviderFormSchema - okta", () => {
|
||||
const validUidFixtures = [
|
||||
"acme.okta.com",
|
||||
"acme.oktapreview.com",
|
||||
"acme.okta-emea.com",
|
||||
"agency.okta-gov.com",
|
||||
"agency.okta.mil",
|
||||
"agency.okta-miltest.com",
|
||||
"agency.trex-govcloud.com",
|
||||
"Acme.okta.com",
|
||||
" ACME.OKTA.COM ",
|
||||
"Agency.Okta-Gov.com",
|
||||
];
|
||||
|
||||
it.each(validUidFixtures)("accepts okta-managed org domain %s", (uid) => {
|
||||
const result = addProviderFormSchema.safeParse({
|
||||
providerType: "okta",
|
||||
providerUid: uid,
|
||||
providerAlias: "okta-test",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Acme.okta.com", "acme.okta.com"],
|
||||
[" ACME.OKTA.COM ", "acme.okta.com"],
|
||||
["Agency.Okta-Gov.com", "agency.okta-gov.com"],
|
||||
])("normalizes okta org domain %s to %s", (input, expected) => {
|
||||
const result = addProviderFormSchema.safeParse({
|
||||
providerType: "okta",
|
||||
providerUid: input,
|
||||
providerAlias: "okta-test",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(
|
||||
result.success && "providerUid" in result.data
|
||||
? result.data.providerUid
|
||||
: undefined,
|
||||
).toBe(expected);
|
||||
});
|
||||
|
||||
const invalidUidFixtures = [
|
||||
"https://acme.okta.com",
|
||||
"acme.example.com",
|
||||
"acme.okta.com/path",
|
||||
"",
|
||||
];
|
||||
|
||||
it.each(invalidUidFixtures)("rejects invalid okta org domain %s", (uid) => {
|
||||
const result = addProviderFormSchema.safeParse({
|
||||
providerType: "okta",
|
||||
providerUid: uid,
|
||||
providerAlias: "okta-test",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addCredentialsFormSchema - okta", () => {
|
||||
const BASE_OKTA_VALUES = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: "provider-okta-1",
|
||||
[ProviderCredentialFields.PROVIDER_TYPE]: "okta",
|
||||
} as const;
|
||||
|
||||
it("accepts okta credentials when client id and private key are present", () => {
|
||||
const schema = addCredentialsFormSchema("okta");
|
||||
|
||||
const result = schema.safeParse({
|
||||
...BASE_OKTA_VALUES,
|
||||
[ProviderCredentialFields.OKTA_CLIENT_ID]: "0oa123456789abcdef",
|
||||
[ProviderCredentialFields.OKTA_PRIVATE_KEY]:
|
||||
"-----BEGIN PRIVATE KEY-----\nMIIEvQ...\n-----END PRIVATE KEY-----",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("reports missing okta private key on okta_private_key field", () => {
|
||||
const schema = addCredentialsFormSchema("okta");
|
||||
|
||||
const result = schema.safeParse({
|
||||
...BASE_OKTA_VALUES,
|
||||
[ProviderCredentialFields.OKTA_CLIENT_ID]: "0oa123456789abcdef",
|
||||
[ProviderCredentialFields.OKTA_PRIVATE_KEY]: "",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success) return;
|
||||
|
||||
expect(result.error.issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: [ProviderCredentialFields.OKTA_PRIVATE_KEY],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
+29
-1
@@ -163,6 +163,18 @@ export const addProviderFormSchema = z
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Team ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("okta"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.regex(
|
||||
/^[a-z0-9][a-z0-9-]*\.(okta\.com|oktapreview\.com|okta-emea\.com|okta-gov\.com|okta\.mil|okta-miltest\.com|trex-govcloud\.com)$/,
|
||||
"Org Domain must be an Okta-managed domain (e.g. acme.okta.com), without scheme or path",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -391,7 +403,23 @@ export const addCredentialsFormSchema = (
|
||||
.trim()
|
||||
.min(1, "API Token is required"),
|
||||
}
|
||||
: {}),
|
||||
: providerType === "okta"
|
||||
? {
|
||||
[ProviderCredentialFields.OKTA_CLIENT_ID]:
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Client ID is required"),
|
||||
[ProviderCredentialFields.OKTA_PRIVATE_KEY]:
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(
|
||||
1,
|
||||
"Private Key is required",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.superRefine((data: Record<string, string | undefined>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const PROVIDER_TYPES = [
|
||||
"cloudflare",
|
||||
"openstack",
|
||||
"vercel",
|
||||
"okta",
|
||||
] as const;
|
||||
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
@@ -34,6 +35,7 @@ export const PROVIDER_DISPLAY_NAMES: Record<ProviderType, string> = {
|
||||
cloudflare: "Cloudflare",
|
||||
openstack: "OpenStack",
|
||||
vercel: "Vercel",
|
||||
okta: "Okta",
|
||||
};
|
||||
|
||||
export function getProviderDisplayName(providerId: string): string {
|
||||
|
||||
Reference in new issue
Block a user