mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 14:54:30 +00:00
feat: saml sso ui integration (#8094)
This commit is contained in:
1 parent
2e97e37316
commit
8e635b3bd4
15 files changed
+829
-122
No files matched your search
@@ -0,0 +1 @@
|
||||
export * from "./saml-config-form";
|
||||
@@ -0,0 +1,262 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
|
||||
import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
|
||||
import { AddIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomServerInput } from "@/components/ui/custom";
|
||||
import { SnippetChip } from "@/components/ui/entities";
|
||||
import { FormButtons } from "@/components/ui/form";
|
||||
|
||||
export const SamlConfigForm = ({
|
||||
setIsOpen,
|
||||
id,
|
||||
}: {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
id: string;
|
||||
}) => {
|
||||
const [state, formAction, isPending] = useFormState(
|
||||
id ? updateSamlConfig : createSamlConfig,
|
||||
null,
|
||||
);
|
||||
const [emailDomain, setEmailDomain] = useState("");
|
||||
const [uploadedFile, setUploadedFile] = useState<{
|
||||
name: string;
|
||||
uploaded: boolean;
|
||||
}>({ name: "", uploaded: false });
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success) {
|
||||
toast({
|
||||
title: "Configuration saved successfully",
|
||||
description: state.success,
|
||||
});
|
||||
setIsOpen(false);
|
||||
} else if (state?.errors?.general) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Oops! Something went wrong",
|
||||
description: state.errors.general,
|
||||
});
|
||||
}
|
||||
}, [state, toast, setIsOpen]);
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
setUploadedFile({ name: "", uploaded: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
const isXmlFile =
|
||||
file.name.toLowerCase().endsWith(".xml") ||
|
||||
file.type === "text/xml" ||
|
||||
file.type === "application/xml";
|
||||
|
||||
if (!isXmlFile) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid file type",
|
||||
description: "Please select a valid XML file (.xml extension).",
|
||||
});
|
||||
// Clear the file input
|
||||
event.target.value = "";
|
||||
setUploadedFile({ name: "", uploaded: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
|
||||
// Basic XML validation
|
||||
if (!content.trim().startsWith("<") || !content.includes("</")) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid XML content",
|
||||
description: "The file does not contain valid XML content.",
|
||||
});
|
||||
// Clear the file input
|
||||
event.target.value = "";
|
||||
setUploadedFile({ name: "", uploaded: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the XML content in a hidden input
|
||||
const xmlInput = document.getElementById(
|
||||
"metadata_xml",
|
||||
) as HTMLInputElement;
|
||||
if (xmlInput) {
|
||||
xmlInput.value = content;
|
||||
}
|
||||
|
||||
// Update file state
|
||||
setUploadedFile({ name: file.name, uploaded: true });
|
||||
|
||||
toast({
|
||||
title: "File uploaded successfully",
|
||||
description: "XML metadata file has been loaded.",
|
||||
});
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "File read error",
|
||||
description: "Failed to read the selected file.",
|
||||
});
|
||||
// Clear the file input
|
||||
event.target.value = "";
|
||||
setUploadedFile({ name: "", uploaded: false });
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const acsUrl = emailDomain
|
||||
? `https://app.prowler.pro/saml/sp/consume/${emailDomain}`
|
||||
: "https://app.prowler.pro/saml/sp/consume/your-domain.com";
|
||||
|
||||
return (
|
||||
<form ref={formRef} action={formAction} className="flex flex-col space-y-6">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<div className="space-y-4">
|
||||
<CustomServerInput
|
||||
name="email_domain"
|
||||
label="Email Domain"
|
||||
placeholder="Enter your email domain (e.g., company.com)"
|
||||
labelPlacement="outside"
|
||||
variant="bordered"
|
||||
isRequired={true}
|
||||
isInvalid={!!state?.errors?.email_domain}
|
||||
errorMessage={state?.errors?.email_domain}
|
||||
value={emailDomain}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmailDomain(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-start space-y-2">
|
||||
<span className="text-xs text-default-500">
|
||||
Metadata XML File <span className="text-red-500">*</span>
|
||||
</span>
|
||||
<CustomButton
|
||||
type="button"
|
||||
ariaLabel="Select Metadata XML File"
|
||||
isDisabled={isPending}
|
||||
onPress={() => {
|
||||
const fileInput = document.getElementById(
|
||||
"metadata_xml_file",
|
||||
) as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.click();
|
||||
}
|
||||
}}
|
||||
startContent={<AddIcon size={20} />}
|
||||
className={`h-10 justify-start rounded-medium border-2 text-default-500 ${
|
||||
state?.errors?.metadata_xml
|
||||
? "border-red-500"
|
||||
: uploadedFile.uploaded
|
||||
? "border-green-500 bg-green-50 dark:bg-green-900/20"
|
||||
: "border-default-200"
|
||||
}`}
|
||||
>
|
||||
<span className="text-small">
|
||||
{uploadedFile.uploaded ? (
|
||||
<span className="flex items-center space-x-2">
|
||||
<span className="max-w-36 truncate">{uploadedFile.name}</span>
|
||||
</span>
|
||||
) : (
|
||||
"Choose File"
|
||||
)}
|
||||
</span>
|
||||
</CustomButton>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="metadata_xml_file"
|
||||
name="metadata_xml_file"
|
||||
accept=".xml,application/xml,text/xml"
|
||||
className="hidden"
|
||||
disabled={isPending}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<input type="hidden" id="metadata_xml" name="metadata_xml" />
|
||||
<p className="text-xs text-gray-500">
|
||||
Upload your Identity Provider's SAML metadata XML file
|
||||
</p>
|
||||
<span className="text-xs text-red-500">
|
||||
{state?.errors?.metadata_xml}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
|
||||
<h3 className="text-lg font-semibold">
|
||||
Identity Provider Configuration
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="mb-2 block text-sm font-medium text-default-500">
|
||||
ACS URL:
|
||||
</span>
|
||||
<SnippetChip
|
||||
value={acsUrl}
|
||||
ariaLabel="Copy ACS URL to clipboard"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mb-2 block text-sm font-medium text-default-500">
|
||||
Audience:
|
||||
</span>
|
||||
<SnippetChip
|
||||
value="urn:prowler.com:sp"
|
||||
ariaLabel="Copy Audience to clipboard"
|
||||
className="w-full"
|
||||
/>
|
||||
</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:
|
||||
</span>
|
||||
<ul className="ml-4 space-y-1 text-sm text-default-600">
|
||||
<li>• firstName</li>
|
||||
<li>• lastName</li>
|
||||
<li>• userType</li>
|
||||
<li>• organization</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButtons setIsOpen={setIsOpen} submitText={id ? "Update" : "Save"} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user