import { Alert, AlertVariant, ButtonVariant, Divider, DropdownItem, Label, PageSection, Tab, TabTitleText, Tooltip, } from "@patternfly/react-core"; import { InfoCircleIcon } from "@patternfly/react-icons"; import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import { cloneDeep, sortBy } from "lodash-es"; import React, { useMemo, useState } from "react"; import { Controller, FormProvider, useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useHistory, useParams } from "react-router-dom"; import { useAlerts } from "../components/alert/Alerts"; import { ConfirmDialogModal, useConfirmDialog, } from "../components/confirm-dialog/ConfirmDialog"; import { DownloadDialog } from "../components/download-dialog/DownloadDialog"; import { stringToMultiline, toStringValue, } from "../components/multi-line-input/multi-line-convert"; import { ViewHeader, ViewHeaderBadge, } from "../components/view-header/ViewHeader"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { RolesList } from "../realm-roles/RolesList"; import { convertFormValuesToObject, convertToFormValues, exportClient, } from "../util"; import useToggle from "../utils/useToggle"; import { AdvancedTab } from "./AdvancedTab"; import { ClientSettings } from "./ClientSettings"; import { ClientSessions } from "./ClientSessions"; import { Credentials } from "./credentials/Credentials"; import { Keys } from "./keys/Keys"; import { ClientParams, ClientTab, toClient } from "./routes/Client"; import { toClients } from "./routes/Clients"; import { ClientScopes } from "./scopes/ClientScopes"; import { EvaluateScopes } from "./scopes/EvaluateScopes"; import { ServiceAccount } from "./service-account/ServiceAccount"; import { isRealmClient, getProtocolName } from "./utils"; import { SamlKeys } from "./keys/SamlKeys"; import { AuthorizationSettings } from "./authorization/Settings"; import { AuthorizationResources } from "./authorization/Resources"; import { AuthorizationScopes } from "./authorization/Scopes"; import { AuthorizationPolicies } from "./authorization/Policies"; import { AuthorizationPermissions } from "./authorization/Permissions"; import { AuthorizationEvaluate } from "./authorization/AuthorizationEvaluate"; import { routableTab, RoutableTabs, } from "../components/routable-tabs/RoutableTabs"; import { AuthorizationTab, toAuthorizationTab, } from "./routes/AuthenticationTab"; import { toClientScopesTab } from "./routes/ClientScopeTab"; import { AuthorizationExport } from "./authorization/AuthorizationExport"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { PermissionsTab } from "../components/permission-tab/PermissionTab"; import type { KeyValueType } from "../components/key-value-form/key-value-convert"; import { useAccess } from "../context/access/Access"; type ClientDetailHeaderProps = { onChange: (value: boolean) => void; value: boolean; save: () => void; client: ClientRepresentation; toggleDownloadDialog: () => void; toggleDeleteDialog: () => void; }; const ClientDetailHeader = ({ onChange, value, save, client, toggleDownloadDialog, toggleDeleteDialog, }: ClientDetailHeaderProps) => { const { t } = useTranslation("clients"); const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({ titleKey: "clients:disableConfirmTitle", messageKey: "clients:disableConfirm", continueButtonLabel: "common:disable", onConfirm: () => { onChange(!value); save(); }, }); const badges = useMemo(() => { const protocolName = getProtocolName( t, client.protocol ?? "openid-connect" ); const text = client.bearerOnly ? ( ) : ( ); return [{ text }]; }, [client, t]); const { hasAccess } = useAccess(); const isManager = hasAccess("manage-clients") || client.access?.configure; const dropdownItems = [ {t("downloadAdapterConfig")} , exportClient(client)}> {t("common:export")} , ...(!isRealmClient(client) && isManager ? [ , {t("common:delete")} , ] : []), ]; return ( <> { if (!value) { toggleDisableDialog(); } else { onChange(value); save(); } }} /> ); }; export type SaveOptions = { confirmed?: boolean; messageKey?: string; }; export default function ClientDetails() { const { t } = useTranslation("clients"); const { adminClient } = useAdminClient(); const { addAlert, addError } = useAlerts(); const { realm } = useRealm(); const { profileInfo } = useServerInfo(); const { hasAccess } = useAccess(); const permissionsEnabled = !profileInfo?.disabledFeatures?.includes("ADMIN_FINE_GRAINED_AUTHZ") && hasAccess("manage-authorization"); const hasManageClients = hasAccess("manage-clients"); const hasViewUsers = hasAccess("view-users"); const history = useHistory(); const [downloadDialogOpen, toggleDownloadDialogOpen] = useToggle(); const [changeAuthenticatorOpen, toggleChangeAuthenticatorOpen] = useToggle(); const form = useForm({ shouldUnregister: false }); const { clientId } = useParams(); const [key, setKey] = useState(0); const clientAuthenticatorType = useWatch({ control: form.control, name: "clientAuthenticatorType", defaultValue: "client-secret", }); const [client, setClient] = useState(); const loader = async () => { const roles = await adminClient.clients.listRoles({ id: clientId }); return sortBy(roles, (role) => role.name?.toUpperCase()); }; const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: "clients:clientDeleteConfirmTitle", messageKey: "clients:clientDeleteConfirm", continueButtonLabel: "common:delete", continueButtonVariant: ButtonVariant.danger, onConfirm: async () => { try { await adminClient.clients.del({ id: clientId }); addAlert(t("clientDeletedSuccess"), AlertVariant.success); history.push(toClients({ realm })); } catch (error) { addError("clients:clientDeleteError", error); } }, }); const setupForm = (client: ClientRepresentation) => { form.reset({ ...client }); convertToFormValues(client, form.setValue); form.setValue( "attributes.request.uris", stringToMultiline(client.attributes?.["request.uris"]) ); if (client.attributes?.["acr.loa.map"]) { form.setValue( "attributes.acr.loa.map", Object.entries(JSON.parse(client.attributes["acr.loa.map"])).flatMap( ([key, value]) => ({ key, value }) ) ); } if (client.attributes?.["default.acr.values"]) { form.setValue( "attributes.default.acr.values", stringToMultiline(client.attributes["default.acr.values"]) ); } if (client.attributes?.["post.logout.redirect.uris"]) { form.setValue( "attributes.post.logout.redirect.uris", stringToMultiline(client.attributes["post.logout.redirect.uris"]) ); } Object.entries(client.attributes || {}) .filter(([key]) => key.startsWith("saml.server.signature")) .map(([key, value]) => form.setValue("attributes." + key.replaceAll(".", "$"), value) ); }; useFetch( () => adminClient.clients.findOne({ id: clientId }), (fetchedClient) => { if (!fetchedClient) { throw new Error(t("common:notFound")); } setClient(cloneDeep(fetchedClient)); setupForm(fetchedClient); }, [clientId, key] ); const save = async ( { confirmed = false, messageKey = "clientSaveSuccess" }: SaveOptions = { confirmed: false, messageKey: "clientSaveSuccess", } ) => { if (await form.trigger()) { if ( !client?.publicClient && client?.clientAuthenticatorType !== clientAuthenticatorType && !confirmed ) { toggleChangeAuthenticatorOpen(); return; } const values = form.getValues(); if (values.attributes?.request.uris) { values.attributes["request.uris"] = toStringValue( values.attributes.request.uris ); } if (values.attributes?.default?.acr?.values) { values.attributes["default.acr.values"] = toStringValue( values.attributes.default.acr.values ); } if (values.attributes?.post.logout.redirect.uris) { values.attributes["post.logout.redirect.uris"] = toStringValue( values.attributes.post.logout.redirect.uris ); } const submittedClient = convertFormValuesToObject(values); Object.entries(values.attributes || {}) .filter(([key]) => key.includes("$")) .map( ([key, value]) => (submittedClient.attributes![key.replaceAll("$", ".")] = value) ); if (submittedClient.attributes?.["acr.loa.map"]) { submittedClient.attributes["acr.loa.map"] = JSON.stringify( Object.fromEntries( (submittedClient.attributes["acr.loa.map"] as KeyValueType[]) .filter(({ key }) => key !== "") .map(({ key, value }) => [key, value]) ) ); } try { const newClient: ClientRepresentation = { ...client, ...submittedClient, }; newClient.clientId = newClient.clientId?.trim(); await adminClient.clients.update({ id: clientId }, newClient); setupForm(newClient); setClient(newClient); addAlert(t(messageKey), AlertVariant.success); } catch (error) { addError("clients:clientSaveError", error); } } }; if (!client) { return ; } const route = (tab: ClientTab) => routableTab({ to: toClient({ realm, clientId, tab, }), history, }); const authenticationRoute = (tab: AuthorizationTab) => routableTab({ to: toAuthorizationTab({ realm, clientId, tab, }), history, }); return ( <> save({ confirmed: true })} > <> {t("changeAuthenticatorConfirm", { clientAuthenticatorType: clientAuthenticatorType, })} {clientAuthenticatorType === "client-jwt" && ( )} ( )} /> {t("common:settings")}} {...route("settings")} > save()} reset={() => setupForm(client)} /> {((!client.publicClient && !isRealmClient(client)) || client.protocol === "saml") && ( {t("keys")}} {...route("keys")} > {client.protocol === "openid-connect" && ( )} {client.protocol === "saml" && ( )} )} {!client.publicClient && !isRealmClient(client) && (hasManageClients || client.access?.configure) && ( {t("credentials")}} {...route("credentials")} > setKey(key + 1)} /> )} {t("roles")}} {...route("roles")} > {!isRealmClient(client) && !client.bearerOnly && ( {t("clientScopes")}} {...route("clientScopes")} > {t("setup")}} {...routableTab({ to: toClientScopesTab({ realm, clientId, tab: "setup", }), history, })} > {t("evaluate")}} {...routableTab({ to: toClientScopesTab({ realm, clientId, tab: "evaluate", }), history, })} > )} {client!.authorizationServicesEnabled && ( {t("authorization")}} {...route("authorization")} > {t("settings")}} {...authenticationRoute("settings")} > {t("resources")}} {...authenticationRoute("resources")} > {t("scopes")}} {...authenticationRoute("scopes")} > {t("policies")}} {...authenticationRoute("policies")} > {t("common:permissions")} } {...authenticationRoute("permissions")} > {t("evaluate")}} {...authenticationRoute("evaluate")} > {t("common:export")}} {...authenticationRoute("export")} > )} {client!.serviceAccountsEnabled && hasViewUsers && ( {t("serviceAccount")}} {...route("serviceAccount")} > )} {t("sessions")}} {...route("sessions")} > {permissionsEnabled && (hasManageClients || client.access?.manage) && ( {t("common:permissions")}} {...route("permissions")} > )} {t("advanced")}} {...route("advanced")} > ); }