keycloak-scim/js/apps/admin-ui/src/clients/keys/SamlKeys.tsx

276 lines
8 KiB
TypeScript
Raw Normal View History

import type CertificateRepresentation from "@keycloak/keycloak-admin-client/lib/defs/certificateRepresentation";
import {
ActionGroup,
AlertVariant,
Button,
Card,
CardBody,
Form,
FormGroup,
PageSection,
Switch,
Text,
TextContent,
} from "@patternfly/react-core";
import { saveAs } from "file-saver";
import { Fragment, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
import { adminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { FormAccess } from "../../components/form/FormAccess";
import { FormPanel } from "../../components/scroll-form/FormPanel";
import { convertAttributeNameToForm } from "../../util";
import { useFetch } from "../../utils/useFetch";
2022-10-26 13:31:33 +00:00
import useToggle from "../../utils/useToggle";
import { FormFields } from "../ClientDetails";
import { Certificate } from "./Certificate";
2022-10-26 13:31:33 +00:00
import { ExportSamlKeyDialog } from "./ExportSamlKeyDialog";
import { SamlImportKeyDialog } from "./SamlImportKeyDialog";
import { SamlKeysDialog } from "./SamlKeysDialog";
type SamlKeysProps = {
clientId: string;
save: () => void;
};
const KEYS = ["saml.signing", "saml.encryption"] as const;
export type KeyTypes = (typeof KEYS)[number];
const KEYS_MAPPING: { [key in KeyTypes]: { [index: string]: string } } = {
"saml.signing": {
name: convertAttributeNameToForm("attributes.saml.client.signature"),
title: "signingKeysConfig",
key: "clientSignature",
},
"saml.encryption": {
name: convertAttributeNameToForm("attributes.saml.encrypt"),
title: "encryptionKeysConfig",
key: "encryptAssertions",
},
};
type KeySectionProps = {
2022-10-26 13:31:33 +00:00
clientId: string;
keyInfo?: CertificateRepresentation;
attr: KeyTypes;
onChanged: (key: KeyTypes) => void;
onGenerate: (key: KeyTypes, regenerate: boolean) => void;
onImport: (key: KeyTypes) => void;
};
const KeySection = ({
2022-10-26 13:31:33 +00:00
clientId,
keyInfo,
attr,
onChanged,
onGenerate,
onImport,
}: KeySectionProps) => {
const { t } = useTranslation();
const { control, watch } = useFormContext<FormFields>();
const title = KEYS_MAPPING[attr].title;
const key = KEYS_MAPPING[attr].key;
const name = KEYS_MAPPING[attr].name;
2022-10-26 13:31:33 +00:00
const [showImportDialog, toggleImportDialog] = useToggle();
const section = watch(name as keyof FormFields);
return (
<>
2022-10-26 13:31:33 +00:00
{showImportDialog && (
<ExportSamlKeyDialog clientId={clientId} close={toggleImportDialog} />
)}
<FormPanel title={t(title)} className="kc-form-panel__panel">
<TextContent className="pf-u-pb-lg">
<Text>{t(`${title}Explain`)}</Text>
</TextContent>
<FormAccess role="manage-clients" isHorizontal>
<FormGroup
labelIcon={
<HelpItem
2023-03-07 09:29:40 +00:00
helpText={t(`clients-help:${key}`)}
2021-12-14 14:56:36 +00:00
fieldLabelId={`clients:${key}`}
/>
}
label={t(key)}
fieldId={key}
hasNoPaddingTop
>
<Controller
name={name as keyof FormFields}
control={control}
defaultValue="false"
render={({ field }) => (
<Switch
data-testid={key}
id={key}
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => {
const v = value.toString();
if (v === "true") {
onChanged(attr);
field.onChange(v);
} else {
onGenerate(attr, false);
}
}}
aria-label={t(key)}
/>
)}
/>
</FormGroup>
</FormAccess>
</FormPanel>
{keyInfo?.certificate && section === "true" && (
<Card isFlat>
<CardBody className="kc-form-panel__body">
<Form isHorizontal>
<Certificate keyInfo={keyInfo} />
<ActionGroup>
<Button
variant="secondary"
onClick={() => onGenerate(attr, true)}
>
{t("regenerate")}
</Button>
<Button variant="secondary" onClick={() => onImport(attr)}>
{t("importKey")}
</Button>
2022-10-26 13:31:33 +00:00
<Button variant="tertiary" onClick={toggleImportDialog}>
{t("common:export")}
</Button>
</ActionGroup>
</Form>
</CardBody>
</Card>
)}
</>
);
};
export const SamlKeys = ({ clientId, save }: SamlKeysProps) => {
const { t } = useTranslation();
const [isChanged, setIsChanged] = useState<KeyTypes>();
const [keyInfo, setKeyInfo] = useState<CertificateRepresentation[]>();
const [selectedType, setSelectedType] = useState<KeyTypes>();
const [openImport, setImportOpen] = useState<KeyTypes>();
const [refresh, setRefresh] = useState(0);
const { setValue } = useFormContext();
const { addAlert, addError } = useAlerts();
useFetch(
() =>
Promise.all(
KEYS.map((attr) =>
adminClient.clients.getKeyInfo({ id: clientId, attr }),
),
),
(info) => setKeyInfo(info),
[refresh],
);
const generate = async (attr: KeyTypes) => {
const index = KEYS.indexOf(attr);
try {
const info = [...(keyInfo || [])];
info[index] = await adminClient.clients.generateKey({
id: clientId,
attr,
});
setKeyInfo(info);
saveAs(
new Blob([info[index].privateKey!], {
type: "application/octet-stream",
}),
"private.key",
);
addAlert(t("generateSuccess"), AlertVariant.success);
} catch (error) {
addError("generateError", error);
}
};
const key = selectedType ? KEYS_MAPPING[selectedType].key : "";
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
titleKey: t("disableSigning", {
key: t(key),
}),
messageKey: t("disableSigningExplain", {
key: t(key),
}),
continueButtonLabel: "common:yes",
cancelButtonLabel: "common:no",
onConfirm: () => {
setValue(KEYS_MAPPING[selectedType!].name, "false");
save();
},
});
const [toggleReGenerateDialog, ReGenerateConfirm] = useConfirmDialog({
titleKey: "reGenerateSigning",
messageKey: "reGenerateSigningExplain",
continueButtonLabel: "common:yes",
cancelButtonLabel: "common:no",
onConfirm: () => {
generate(selectedType!);
},
});
return (
<PageSection variant="light" className="keycloak__form">
{isChanged && (
<SamlKeysDialog
id={clientId}
attr={isChanged}
onClose={() => {
setIsChanged(undefined);
save();
setRefresh(refresh + 1);
}}
2021-11-15 07:30:53 +00:00
onCancel={() => {
setValue(KEYS_MAPPING[selectedType!].name, "false");
setIsChanged(undefined);
}}
/>
)}
<DisableConfirm />
<ReGenerateConfirm />
{KEYS.map((attr, index) => (
2021-10-14 10:30:49 +00:00
<Fragment key={attr}>
{openImport === attr && (
<SamlImportKeyDialog
id={clientId}
attr={attr}
onClose={() => setImportOpen(undefined)}
/>
)}
<KeySection
2022-10-26 13:31:33 +00:00
clientId={clientId}
keyInfo={keyInfo?.[index]}
attr={attr}
onChanged={setIsChanged}
onGenerate={(type, isNew) => {
setSelectedType(type);
if (!isNew) {
toggleDisableDialog();
} else {
toggleReGenerateDialog();
}
}}
onImport={() => setImportOpen(attr)}
/>
2021-10-14 10:30:49 +00:00
</Fragment>
))}
</PageSection>
);
};