From c60c83b6cce675be7484ee8979cb964d11031d98 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Thu, 4 Aug 2022 13:08:11 +0200 Subject: [PATCH] Added posibilty to import SAML XML (#3049) --- public/resources/en/clients.json | 1 + public/resources/en/common-help.json | 1 + src/clients/import/ImportForm.tsx | 74 +++++++++++++++---- src/i18n.ts | 7 +- .../add/OpenIdConnectSettings.tsx | 5 +- .../add/SamlConnectSettings.tsx | 5 +- src/utils/getAuthorizationHeaders.ts | 9 +++ 7 files changed, 77 insertions(+), 25 deletions(-) create mode 100644 src/utils/getAuthorizationHeaders.ts diff --git a/public/resources/en/clients.json b/public/resources/en/clients.json index 1f6fae73ca..d72def6432 100644 --- a/public/resources/en/clients.json +++ b/public/resources/en/clients.json @@ -503,6 +503,7 @@ "importFile": "Import file", "importSuccess": "New certificate imported", "importError": "Could not import certificate {{error}}", + "importParseError": "Could not parse the file {{error}}", "tokenLifespan": { "expires": "Expires in", "never": "Never expires" diff --git a/public/resources/en/common-help.json b/public/resources/en/common-help.json index bd3b278d06..e26965c624 100644 --- a/public/resources/en/common-help.json +++ b/public/resources/en/common-help.json @@ -2,5 +2,6 @@ "helpToggleInfo": "This toggle will enable / disable part of the help info in the console. Includes any help text, links and popovers.", "showPassword": "Show password field in clear text", "helpFileUpload": "Upload a JSON file", + "helpFileUploadClient": "Upload a JSON or XML file", "dragHelp": "Press space or enter to begin dragging, and use the arrow keys to navigate up or down. Press enter to confirm the drag, or any other key to cancel the drag operation." } \ No newline at end of file diff --git a/src/clients/import/ImportForm.tsx b/src/clients/import/ImportForm.tsx index ea1c8e5788..985cc7cb85 100644 --- a/src/clients/import/ImportForm.tsx +++ b/src/clients/import/ImportForm.tsx @@ -1,3 +1,7 @@ +import { useState } from "react"; +import { Link, useHistory } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { FormProvider, useForm } from "react-hook-form"; import { ActionGroup, AlertVariant, @@ -5,23 +9,29 @@ import { FormGroup, PageSection, } from "@patternfly/react-core"; +import { Language } from "@patternfly/react-code-editor"; + import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; -import { useState } from "react"; -import { FormProvider, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { Link, useHistory } from "react-router-dom"; + import { useAlerts } from "../../components/alert/Alerts"; import { FormAccess } from "../../components/form-access/FormAccess"; -import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload"; import { ViewHeader } from "../../components/view-header/ViewHeader"; import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; import { useAdminClient } from "../../context/auth/AdminClient"; import { useRealm } from "../../context/realm-context/RealmContext"; -import { convertFormValuesToObject, convertToFormValues } from "../../util"; +import { + addTrailingSlash, + convertFormValuesToObject, + convertToFormValues, +} from "../../util"; import { CapabilityConfig } from "../add/CapabilityConfig"; import { ClientDescription } from "../ClientDescription"; import { toClient } from "../routes/Client"; import { toClients } from "../routes/Clients"; +import { FileUploadForm } from "../../components/json-file-upload/FileUploadForm"; +import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders"; + +const isXml = (text: string) => text.startsWith("<"); export default function ImportForm() { const { t } = useTranslation("clients"); @@ -34,18 +44,44 @@ export default function ImportForm() { const { addAlert, addError } = useAlerts(); - const handleFileChange = (obj?: object) => { - const defaultClient = { - protocol: "", - clientId: "", - name: "", - description: "", - }; + const handleFileChange = async (contents: string) => { + try { + const parsed = await parseFileContents(contents); - convertToFormValues(obj || defaultClient, setValue); - setImported(obj || defaultClient); + convertToFormValues(parsed, setValue); + setImported(parsed); + } catch (error) { + addError("clients:importParseError", error); + } }; + async function parseFileContents( + contents: string + ): Promise { + if (!isXml(contents)) { + return JSON.parse(contents); + } + + const response = await fetch( + `${addTrailingSlash( + adminClient.baseUrl + )}admin/realms/${realm}/client-description-converter`, + { + method: "POST", + body: contents, + headers: getAuthorizationHeaders(await adminClient.getAccessToken()), + } + ); + + if (!response.ok) { + throw new Error( + `Server responded with invalid status: ${response.statusText}` + ); + } + + return response.json(); + } + const save = async (client: ClientRepresentation) => { try { const newClient = await adminClient.clients.create({ @@ -74,7 +110,13 @@ export default function ImportForm() { role="manage-clients" > - + { const { t } = useTranslation("identity-providers"); @@ -45,9 +46,7 @@ export const OpenIdConnectSettings = () => { { method: "POST", body: formData, - headers: { - Authorization: `Bearer ${await adminClient.getAccessToken()}`, - }, + headers: getAuthorizationHeaders(await adminClient.getAccessToken()), } ); if (response.ok) { diff --git a/src/identity-providers/add/SamlConnectSettings.tsx b/src/identity-providers/add/SamlConnectSettings.tsx index 0802818c33..7fc5ca87ae 100644 --- a/src/identity-providers/add/SamlConnectSettings.tsx +++ b/src/identity-providers/add/SamlConnectSettings.tsx @@ -13,6 +13,7 @@ import { DiscoveryEndpointField } from "../component/DiscoveryEndpointField"; import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; import environment from "../../environment"; import { addTrailingSlash } from "../../util"; +import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders"; export const SamlConnectSettings = () => { const { t } = useTranslation("identity-providers"); @@ -51,9 +52,7 @@ export const SamlConnectSettings = () => { { method: "POST", body: formData, - headers: { - Authorization: `bearer ${await adminClient.getAccessToken()}`, - }, + headers: getAuthorizationHeaders(await adminClient.getAccessToken()), } ); if (response.ok) { diff --git a/src/utils/getAuthorizationHeaders.ts b/src/utils/getAuthorizationHeaders.ts new file mode 100644 index 0000000000..cbf7aeae40 --- /dev/null +++ b/src/utils/getAuthorizationHeaders.ts @@ -0,0 +1,9 @@ +export function getAuthorizationHeaders( + accessToken?: string +): Record { + if (!accessToken) { + return {}; + } + + return { Authorization: `Bearer ${accessToken}` }; +}