mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat: saml e2e improvements (#8158)
This commit is contained in:
@@ -1,20 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
|
||||
|
||||
const samlConfigFormSchema = z.object({
|
||||
email_domain: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Email domain is required" }),
|
||||
metadata_xml: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Metadata XML is required" }),
|
||||
});
|
||||
import { samlConfigFormSchema } from "@/types/formSchemas";
|
||||
|
||||
export const createSamlConfig = async (_prevState: any, formData: FormData) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
@@ -156,6 +145,39 @@ export const getSamlConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteSamlConfig = async (id: string) => {
|
||||
const headers = await getAuthHeaders({ contentType: true });
|
||||
|
||||
try {
|
||||
const url = new URL(`${apiBaseUrl}/saml-config/${id}`);
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "DELETE",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.errors?.[0]?.detail ||
|
||||
`Failed to delete SAML config: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
revalidatePath("/integrations");
|
||||
return { success: "SAML configuration deleted successfully!" };
|
||||
} catch (error) {
|
||||
console.error("Error deleting SAML config:", error);
|
||||
return {
|
||||
errors: {
|
||||
general:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error deleting SAML configuration. Please try again.",
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const initiateSamlAuth = async (email: string) => {
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/auth/saml/initiate/`, {
|
||||
|
||||
@@ -85,7 +85,7 @@ const SSRDataUser = async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full pr-0 lg:w-2/3 xl:w-1/2 xl:pr-3">
|
||||
<SamlIntegrationCard id={samlConfig.data[0]?.id} />
|
||||
<SamlIntegrationCard samlConfig={samlConfig?.data?.[0]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
|
||||
@@ -13,16 +14,18 @@ import { apiBaseUrl } from "@/lib";
|
||||
|
||||
export const SamlConfigForm = ({
|
||||
setIsOpen,
|
||||
id,
|
||||
samlConfig,
|
||||
}: {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
id: string;
|
||||
samlConfig?: any;
|
||||
}) => {
|
||||
const [state, formAction, isPending] = useFormState(
|
||||
id ? updateSamlConfig : createSamlConfig,
|
||||
samlConfig?.id ? updateSamlConfig : createSamlConfig,
|
||||
null,
|
||||
);
|
||||
const [emailDomain, setEmailDomain] = useState("");
|
||||
const [emailDomain, setEmailDomain] = useState(
|
||||
samlConfig?.attributes?.email_domain || "",
|
||||
);
|
||||
const [uploadedFile, setUploadedFile] = useState<{
|
||||
name: string;
|
||||
uploaded: boolean;
|
||||
@@ -126,7 +129,7 @@ export const SamlConfigForm = ({
|
||||
|
||||
return (
|
||||
<form ref={formRef} action={formAction} className="flex flex-col space-y-2">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<input type="hidden" name="id" value={samlConfig?.id || ""} />
|
||||
<CustomServerInput
|
||||
name="email_domain"
|
||||
label="Email Domain"
|
||||
@@ -170,17 +173,6 @@ export const SamlConfigForm = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mb-2 block text-sm font-medium text-default-500">
|
||||
Name ID Format:
|
||||
</span>
|
||||
<SnippetChip
|
||||
value="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
||||
ariaLabel="Copy Name ID Format to clipboard"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mb-2 block text-sm font-medium text-default-500">
|
||||
Supported Assertion Attributes:
|
||||
@@ -195,7 +187,11 @@ export const SamlConfigForm = ({
|
||||
<strong>Note:</strong> The userType attribute will be used to
|
||||
assign the user's role. If the role does not exist, one will
|
||||
be created with minimal permissions. You can assign permissions to
|
||||
roles on the Roles page.
|
||||
roles on the{" "}
|
||||
<Link href="/roles">
|
||||
<span className="underline">Roles</span>
|
||||
</Link>{" "}
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,7 +249,10 @@ export const SamlConfigForm = ({
|
||||
{state?.errors?.metadata_xml}
|
||||
</span>
|
||||
</div>
|
||||
<FormButtons setIsOpen={setIsOpen} submitText={id ? "Update" : "Save"} />
|
||||
<FormButtons
|
||||
setIsOpen={setIsOpen}
|
||||
submitText={samlConfig?.id ? "Update" : "Save"}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,15 +2,50 @@
|
||||
|
||||
import { Card, CardBody, CardHeader } from "@nextui-org/react";
|
||||
import { Link } from "@nextui-org/react";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { CheckIcon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { deleteSamlConfig } from "@/actions/integrations";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
|
||||
|
||||
import { SamlConfigForm } from "./forms";
|
||||
|
||||
export const SamlIntegrationCard = ({ id }: { id: string }) => {
|
||||
export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => {
|
||||
const [isSamlModalOpen, setIsSamlModalOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const id = samlConfig?.id;
|
||||
|
||||
const handleRemoveSaml = async () => {
|
||||
if (!id) return;
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const result = await deleteSamlConfig(id);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "SAML configuration removed",
|
||||
description: result.success,
|
||||
});
|
||||
} else if (result.errors?.general) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error removing SAML configuration",
|
||||
description: result.errors.general,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to remove SAML configuration. Please try again.",
|
||||
});
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -19,7 +54,10 @@ export const SamlIntegrationCard = ({ id }: { id: string }) => {
|
||||
onOpenChange={setIsSamlModalOpen}
|
||||
title="Configure SAML SSO"
|
||||
>
|
||||
<SamlConfigForm setIsOpen={setIsSamlModalOpen} id={id} />
|
||||
<SamlConfigForm
|
||||
setIsOpen={setIsSamlModalOpen}
|
||||
samlConfig={samlConfig}
|
||||
/>
|
||||
</CustomAlertModal>
|
||||
|
||||
<Card className="dark:bg-prowler-blue-400">
|
||||
@@ -56,14 +94,29 @@ export const SamlIntegrationCard = ({ id }: { id: string }) => {
|
||||
{id ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</div>
|
||||
<CustomButton
|
||||
size="sm"
|
||||
ariaLabel="Add SAML SSO"
|
||||
color="action"
|
||||
onPress={() => setIsSamlModalOpen(true)}
|
||||
>
|
||||
{id ? "Update" : "Enable"}
|
||||
</CustomButton>
|
||||
<div className="flex gap-2">
|
||||
<CustomButton
|
||||
size="sm"
|
||||
ariaLabel="Configure SAML SSO"
|
||||
color="action"
|
||||
onPress={() => setIsSamlModalOpen(true)}
|
||||
>
|
||||
{id ? "Update" : "Enable"}
|
||||
</CustomButton>
|
||||
{id && (
|
||||
<CustomButton
|
||||
size="sm"
|
||||
ariaLabel="Remove SAML SSO"
|
||||
color="danger"
|
||||
variant="bordered"
|
||||
isLoading={isDeleting}
|
||||
startContent={!isDeleting ? <Trash2Icon size={16} /> : null}
|
||||
onPress={handleRemoveSaml}
|
||||
>
|
||||
Remove
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
@@ -309,3 +309,14 @@ export const editUserFormSchema = () =>
|
||||
userId: z.string(),
|
||||
role: z.string().optional(),
|
||||
});
|
||||
|
||||
export const samlConfigFormSchema = z.object({
|
||||
email_domain: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Email domain is required" }),
|
||||
metadata_xml: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "Metadata XML is required" }),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user